前言
虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型此衅,這就是虛擬機(jī)的類加載機(jī)制。
-
類加載的流程
類從被加載到虛擬機(jī)內(nèi)存中開(kāi)始亭螟,到卸載出內(nèi)存位置挡鞍,它的整個(gè)生命周期包括:加載、驗(yàn)證预烙、準(zhǔn)備匕累、解析、初始化默伍、使用和卸載欢嘿,其中驗(yàn)證衰琐、準(zhǔn)備、解析三個(gè)部分統(tǒng)稱為連接炼蹦。這七個(gè)階段的發(fā)生順序如圖1-1所示羡宙。
圖1-1:類加載流程圖
上圖中,加載掐隐、驗(yàn)證狗热、準(zhǔn)備、初始化和卸載這5個(gè)階段的順序是固定的虑省,類的加載過(guò)程必須按照這種順序按部就班地開(kāi)始匿刮,但是解析階段則不一定:他在否種情況下可以再初始化階段之后再開(kāi)始,這是為了支持Java語(yǔ)言的運(yùn)行時(shí)綁定探颈。同事熟丸,上面這是階段通常都是互相交叉地混合進(jìn)行的,通常會(huì)在一個(gè)階段執(zhí)行的過(guò)程中調(diào)用伪节、激活另一個(gè)階段(例如在一個(gè)類的內(nèi)部初始化另一個(gè)類)光羞。
-
類加載的時(shí)機(jī)
什么情況下需要開(kāi)始類加載過(guò)程的第一個(gè)階段:加載?Java虛擬機(jī)規(guī)范中并沒(méi)有進(jìn)行強(qiáng)制約束怀大,這點(diǎn)交給虛擬機(jī)的具體實(shí)現(xiàn)來(lái)自由把握纱兑。但是對(duì)于初始化階段,虛擬機(jī)規(guī)范則是嚴(yán)格規(guī)定了有且只有5中情況必須立即對(duì)類進(jìn)行初始化(加載化借、驗(yàn)證潜慎、準(zhǔn)備自然需要在此之前開(kāi)始)。
- 遇到
new 蓖康、getstatic勘纯、putstatic、invokestatic
這四條字節(jié)碼指令時(shí)钓瞭,如果類沒(méi)有進(jìn)行國(guó)儲(chǔ)石化,則需要先觸發(fā)其初始化淫奔。生成這四條指令的場(chǎng)景是:使用new
關(guān)鍵字實(shí)例化對(duì)象山涡,讀取或這只一個(gè)類的靜態(tài)變量(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候唆迁,以及調(diào)用一個(gè)類的靜態(tài)方法的時(shí)候鸭丛。 - 使用
java.lang.reflect
包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候,如果類沒(méi)有經(jīng)過(guò)初始化唐责,則需要先觸發(fā)其初始化 - 當(dāng)初始化一個(gè)類的時(shí)候鳞溉,如果其父類還沒(méi)有經(jīng)過(guò)初始化,則需要先觸發(fā)其父類的初始化鼠哥。
- 虛擬機(jī)啟動(dòng)時(shí)熟菲,用戶需要制定一個(gè)要執(zhí)行的主類(包含main()方法的那個(gè)類)看政,虛擬機(jī)會(huì)先初始化這個(gè)主類。
- 當(dāng)使用JDK1.7的動(dòng)態(tài)語(yǔ)言支持時(shí)抄罕,如果一個(gè)
java.lang.invoke.MethodHandle
實(shí)例最后的解析結(jié)果REF_getStatic允蚣、REF_putStatic、REF_invokeStatic
的方法句柄呆贿,并且這個(gè)方法句柄所對(duì)應(yīng)的類沒(méi)有經(jīng)過(guò)初始化嚷兔,則需要先觸發(fā)其初始化。
對(duì)于以上5種會(huì)觸發(fā)類進(jìn)行初始化的場(chǎng)景做入,虛擬機(jī)規(guī)范中使用了一個(gè)很強(qiáng)烈的現(xiàn)定于:
有且只有
冒晰,這5種場(chǎng)景中的行為被稱為對(duì)一個(gè)類進(jìn)行主動(dòng)引用,但是除此之外竟块,所有引用類的方式都不會(huì)觸發(fā)初始化壶运,稱為被動(dòng)引用,如下面例子: - 遇到
public class Parent {
public static int a = 1;
static {
System.out.println("Parent init");
}
}
public class Son extends Parent{
static {
System.out.println("Son init");
}
}
public static void main(String[] args) {
System.out.println("args = [" + Son.a + "]");
}
輸出結(jié)果:
Parent init
args = [1]
對(duì)于靜態(tài)字段彩郊,只有直接定義這個(gè)字段的類才會(huì)被初始化前弯,因此通過(guò)其子類來(lái)引用父類中定義的靜態(tài)字段,只會(huì)觸發(fā)父類的初始化而不會(huì)觸發(fā)子類的初始化秫逝,至于是否要觸發(fā)子類的加載和驗(yàn)證恕出,在虛擬機(jī)規(guī)范中并未明確規(guī)定,這點(diǎn)取決于虛擬機(jī)的具體實(shí)現(xiàn)违帆,對(duì)于Sun HotSpot虛擬機(jī)來(lái)說(shuō)浙巫,可通過(guò)-XX:_TraceClassLoading
參數(shù)觀察到次操作會(huì)導(dǎo)致子類的加載男应。
除此之外侵续,通過(guò)數(shù)組定義來(lái)引用類,不會(huì)觸發(fā)此類的初始化足淆。
public static void main(String[] args) {
Parent[] parentArry = new Parent[10];
}
運(yùn)行上述代碼后什么輸出也沒(méi)有尝胆,說(shuō)明并沒(méi)有觸發(fā)Parent
類的初始化階段丧裁。但是這段代碼里面觸發(fā)了另一個(gè)名為[Lxxx.xxx.Parent(前面的xxx指代類的包名)
的類的初始化,這里是不是看起來(lái)有點(diǎn)眼熟含衔,在前面字節(jié)碼的文章里可以知道[L
這里表示的是一個(gè)對(duì)象數(shù)組煎娇。它是由虛擬機(jī)自動(dòng)生成的、直接繼承與Object的類贪染,創(chuàng)建動(dòng)作由字節(jié)碼指令newarray
觸發(fā)缓呛。
這個(gè)類表示了一個(gè)元素類型為Parent
的一維數(shù)組,數(shù)組中應(yīng)有的屬性和方法(可被用戶直接調(diào)用的方法只有l(wèi)ength和clone)都實(shí)現(xiàn)在這個(gè)類里。在Java語(yǔ)言中杭隙,當(dāng)檢查到數(shù)組越界時(shí)會(huì)拋出ArrayIndexOutOfBoundsException
異常哟绊,但是這個(gè)異常檢測(cè)不是封裝在數(shù)組元素訪問(wèn)的類中,而是封裝在數(shù)組訪問(wèn)的xaload痰憎、xastore
字節(jié)碼指令中票髓。
當(dāng)引用一個(gè)類的靜態(tài)且被final修飾的常量時(shí)攀涵,不會(huì)觸發(fā)此類的初始化
public class Parent {
public static final int a = 1;
static {
System.out.println("Parent init");
}
}
public static void main(String[] args) {
System.out.println("args = [" + Son.a + "]");
}
輸出結(jié)果:
args = [1]
因?yàn)樽鳛?code>final修飾的常量時(shí)一個(gè)不可變的值,所以在編譯階段會(huì)通過(guò)常量傳播優(yōu)化炬称,將此常量的值1存儲(chǔ)到了主類(main方法所在的類)的常量池中汁果,所以以后主類中對(duì)常量1的引用實(shí)際都被轉(zhuǎn)化了主類對(duì)自身常量池的引用,也就是說(shuō)玲躯,實(shí)際上主類的Class文件中并沒(méi)有Parent
類得符號(hào)引用据德,這兩個(gè)類在編異常Class之后就不存在任何聯(lián)系了。
接口的架子啊過(guò)程與類加載過(guò)程稍有不同跷车,針對(duì)接口需要做一些特殊說(shuō)明:接口也有初始化過(guò)程棘利,這點(diǎn)和類是一致的,但是接口中不能使用static{}
語(yǔ)句塊朽缴,但是編譯器仍然會(huì)為接口生成<client>
類構(gòu)造器善玫,用于初始化接口中所定義的成員變量。接口與類正則有所區(qū)別的是前面講述的需要初始化場(chǎng)景的第三種:當(dāng)一個(gè)類在初始化時(shí)密强,要求其父類全部都已經(jīng)初始化過(guò)了茅郎。但是一個(gè)接口在初始化時(shí),并不要求其負(fù)借口全部都完成了初始化或渤,只有在真正使用到負(fù)借口的時(shí)候(如引用接口中定義的常量)才會(huì)被初始化系冗。
-
類加載的步驟
接下來(lái)詳細(xì)講解一下類加載的全過(guò)程,也就是加載薪鹦、驗(yàn)證掌敬、準(zhǔn)備、解析池磁、初始化這5個(gè)階段鎖執(zhí)行的具體動(dòng)作奔害。
-
加載
加載是類加載過(guò)程的一個(gè)階段,在加載階段地熄,虛擬機(jī)主要完成一下三件事
- 1.通過(guò)一個(gè)類的全限定名來(lái)獲取定義此類的二進(jìn)制字節(jié)流华临。
- 2.將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
- 3.在內(nèi)存中生成一個(gè)代表這個(gè)類的Class對(duì)象端考,作為方法區(qū)這個(gè)類得各種數(shù)據(jù)的訪問(wèn)入口雅潭。
加載階段沒(méi)有規(guī)定加載的內(nèi)容從哪來(lái),因?yàn)樗虞d的是一個(gè)類的全限定名來(lái)獲取定義此類的二進(jìn)制字節(jié)流跛梗。所以,虛擬機(jī)根本沒(méi)有制定要從那里獲取棋弥,怎樣獲取核偿,但是常見(jiàn)的獲取方式有下面幾種:
- 從zip包中獲取,也就是常見(jiàn)的JAR,EAR,WAR
- 從網(wǎng)絡(luò)中獲取顽染,最典型的場(chǎng)景應(yīng)用就是Applet
- 運(yùn)行時(shí)計(jì)算生成漾岳,主要用于動(dòng)態(tài)代理技術(shù)轰绵,在
java.lang.reflect.Proxy
中就是用了ProxyGenerator.gengrateProxyClass
來(lái)為特定接口生成形式為*$Proxy
的代理類的二進(jìn)制字節(jié)流 - 由其他文件生成,例如由JSP文件生成對(duì)應(yīng)的Class類
- 從數(shù)據(jù)庫(kù)中讀取尼荆,例如有些中間件服務(wù)器可以選擇把程序安裝到數(shù)據(jù)庫(kù)中來(lái)完成程序代碼在集群間的分發(fā)左腔。
......
對(duì)于類加載過(guò)程的其他階段,一個(gè)非數(shù)組的加載階段(準(zhǔn)確的說(shuō)捅儒,是加載階段中獲取類的二進(jìn)制字節(jié)流的動(dòng)作)是開(kāi)發(fā)人員可控性最強(qiáng)的液样,因?yàn)榧虞d階段可以使用系統(tǒng)提供的引導(dǎo)類加載器來(lái)完成,也可以由用戶自定義的類加載器去完成(例如對(duì)字節(jié)碼加密巧还,然后通過(guò)自定義類加載器來(lái)解密后加載類)鞭莽,開(kāi)發(fā)人員可以通過(guò)定義自己的類加載器去控制字節(jié)流的獲取方式。
但是數(shù)組類并不是通過(guò)類加載器創(chuàng)建的麸祷,它是由Java虛擬機(jī)直接創(chuàng)建的澎怒。不過(guò)數(shù)據(jù)類型與類加載器仍然有很密切的關(guān)系,因?yàn)閿?shù)組類的元素類型最終還是要考類加載器去創(chuàng)建阶牍,一個(gè)數(shù)組類的創(chuàng)建過(guò)程就遵循以下規(guī)則:
- 1 . 如果數(shù)組的類型時(shí)一個(gè)引用類型喷面,那就需要去加載這個(gè)組件類型,然后在加載該組件類型的類加載器的類名稱空間上被標(biāo)識(shí)走孽,這一點(diǎn)在后續(xù)的類加載器中會(huì)講述到惧辈。
- 2 . 如果數(shù)組的類型時(shí)基礎(chǔ)數(shù)據(jù)類型,Java虛擬機(jī)會(huì)把數(shù)組標(biāo)記為與引導(dǎo)類加載器關(guān)聯(lián)融求。
- 3 . 數(shù)組類的可見(jiàn)性與它的組件類型可見(jiàn)性一致咬像,如果組件類型不是引用類型,那數(shù)組的可見(jiàn)性將默認(rèn)為public生宛。
加載階段完成后县昂,虛擬機(jī)將外部的二進(jìn)制字節(jié)流按照虛擬機(jī)所需的格式存儲(chǔ)在方法區(qū)之中,方法區(qū)中的數(shù)據(jù)存儲(chǔ)格式由虛擬機(jī)自行定義陷舅,然后在內(nèi)存中實(shí)例化一個(gè)Class類的對(duì)象(并沒(méi)有明確是在Java堆中倒彰,對(duì)于HotSpot虛擬機(jī)而言mClass對(duì)象比較特殊,他雖然是對(duì)象莱睁,但是存放在方法區(qū)里面)待讳,這個(gè)對(duì)象將作為程序訪問(wèn)方法區(qū)中的這些類型數(shù)據(jù)的外部接口。
加載階段和后續(xù)的連接階段的部分內(nèi)容是交叉進(jìn)行的仰剿,加載階段尚未完成時(shí)创淡,連接階段可能已經(jīng)開(kāi)始了,但是這些夾在加載階段的動(dòng)作仍然屬于連接階段南吮。
-
驗(yàn)證
驗(yàn)證是連接階段的第一部琳彩,這一步的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。虛擬機(jī)如果不檢查輸入的字節(jié)流,對(duì)其完全信任的話,很可能會(huì)因?yàn)檩d入了有害的字節(jié)流而導(dǎo)致系統(tǒng)崩潰,所以驗(yàn)證是虛擬機(jī)對(duì)自身保護(hù)的一項(xiàng)重要工作。
從2011年發(fā)布的《Java虛擬機(jī)規(guī)范(JSE 7版)》中從整體上上看,研制階段大致上會(huì)完成下面4個(gè)階段的校驗(yàn)動(dòng)作:文件格式校驗(yàn)、元數(shù)據(jù)校驗(yàn)、字節(jié)碼校驗(yàn)、符號(hào)引用驗(yàn)證腻惠。 -
1 . 文件格式驗(yàn)證
驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理欣喧,可能包含下面這些驗(yàn)證點(diǎn):- 是否以魔數(shù)0xCAFEBABY開(kāi)頭锈锤。
- 主次版本號(hào)是否在當(dāng)前虛擬機(jī)處理范圍之內(nèi)。
- 常量池的常量中是否有不被支持的常量類型(檢查常量的tag標(biāo)志)。
- 指向常量的各種索引值是否有指向不存在的常量或不符合類型的常量。
- CONSTANT_Utf8_info類型的常量中是否有不符合UTF8編碼的數(shù)據(jù)。
- Class文件中各個(gè)部分及文件本身是否有被刪除的或附加的其他信息。
......
上面只是驗(yàn)證的一小部分點(diǎn)续崖,目的是包在輸入的字節(jié)流能正確地解析并且格式上符合一個(gè)Java類型的數(shù)據(jù)要求。只有通過(guò)這個(gè)階段的兗州严望,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)進(jìn)行存儲(chǔ)逻恐,后面的三個(gè)驗(yàn)證階段全部是基于方法取得存儲(chǔ)結(jié)構(gòu)進(jìn)行的像吻,不會(huì)再直接操作字節(jié)流复隆。
-
2 . 元數(shù)據(jù)驗(yàn)證
第二步是對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,保證其描述的信息符合Java語(yǔ)言規(guī)范的要求,這個(gè)階段的包含的驗(yàn)證點(diǎn)如下:- 這個(gè)類是否有父類(除了Object,所有的類都應(yīng)該有父類)。
- 這個(gè)類的父類是否繼承了不被允許繼承的類(被final修飾的類)览爵。
- 如果這個(gè)類不是抽象類,是否實(shí)現(xiàn)了其父類或接口中的要求實(shí)現(xiàn)的所有方法。
- 類中的字段俱济、方法是否和父類產(chǎn)生了矛盾(例如覆蓋了父類的final字段)嘶是。
......
-
3 .字節(jié)碼驗(yàn)證
這是驗(yàn)證過(guò)程中最復(fù)雜的一個(gè)階段,主要目的是通過(guò)數(shù)據(jù)流和控制流分析蛛碌,確定程序語(yǔ)義是合法的聂喇、符合邏輯的。交驗(yàn)完元數(shù)據(jù)后蔚携,這個(gè)階段將對(duì)類的方法體進(jìn)行校驗(yàn)分析希太,保證被校驗(yàn)類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的事件,例如:- 操作數(shù)棧的數(shù)據(jù)類型和指令代碼序列能配合工作酝蜒,例如不會(huì)出現(xiàn)操作數(shù)棧存入了int類型數(shù)據(jù)誊辉,加載時(shí)卻用long類型。
- 保證跳轉(zhuǎn)指令(goto)不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上亡脑。
- 保證方法體中的類型轉(zhuǎn)換時(shí)有效的堕澄。
......
如果一個(gè)類方法體沒(méi)通過(guò)校驗(yàn),那肯定是有問(wèn)題的霉咨,但是通過(guò)了校驗(yàn)也不一定是完全安全的奈偏,即
通過(guò)程序去校驗(yàn)程序邏輯是無(wú)法做到絕對(duì)準(zhǔn)確的
。虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)為了避免過(guò)多的時(shí)間消耗在字節(jié)碼校驗(yàn)階段躯护,在JDK1.6之后Javac虛擬機(jī)中進(jìn)行了一項(xiàng)優(yōu)化惊来,給方法體的Code屬性的屬性表中增加了一項(xiàng)名為
StackMapTable
的屬性,這項(xiàng)屬性描述了方法體中所有的基本虧啊開(kāi)始時(shí)本地變量表和操作數(shù)棧應(yīng)有的狀態(tài)棺滞,字節(jié)碼校驗(yàn)期間裁蚁,就不需要根據(jù)程序推導(dǎo)這些狀態(tài)的合法性,只需要檢查StackMapTable
屬性中的記錄是否合法即可继准,這樣將字節(jié)碼驗(yàn)證的類型推導(dǎo)轉(zhuǎn)換為類型檢查枉证,從而節(jié)省一些時(shí)間。 -
4 .符號(hào)引用驗(yàn)證
最后一個(gè)階段校驗(yàn)發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候移必,這個(gè)轉(zhuǎn)化動(dòng)作將在連接的第三階段解析中發(fā)生室谚,符號(hào)引用驗(yàn)證可以看做是對(duì)類自身以外(常量池中的各種符號(hào)引用)的信息進(jìn)行匹配性校驗(yàn),同樣需要校驗(yàn)下列內(nèi)容:
- 符號(hào)引用中通過(guò)字符串描述的全限定名是否能找到對(duì)應(yīng)的類崔泵。
- 在指定類中是否存在符合方法的字段描述符以及簡(jiǎn)單名稱所描述的方法和字段秒赤。
- 符號(hào)引用中的類、字段憎瘸、方法的訪問(wèn)類型是否可被當(dāng)前類訪問(wèn)入篮。
......
符號(hào)引用驗(yàn)證的目的是確保解析動(dòng)作能正常執(zhí)行,如果無(wú)法通過(guò)符號(hào)引用幌甘,那么會(huì)拋出一個(gè)IncompatibleClassChangeError
異常的子類潮售,例如NoSuchField(Method)Error
痊项。
對(duì)于虛擬機(jī)來(lái)說(shuō),驗(yàn)證階段是一個(gè)重要酥诽,但不是必要的階段鞍泉,如果你的代碼已經(jīng)被反復(fù)使用和驗(yàn)證過(guò)了,那么在實(shí)施階段就可以考慮用
-Xverify:none
參數(shù)來(lái)關(guān)閉大部分的類驗(yàn)證措施肮帐,以縮短類加載的時(shí)間塞弊。 -
準(zhǔn)備
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配泪姨。這個(gè)階段有兩個(gè)容易混淆的概念需要強(qiáng)調(diào)一下:首先,這個(gè)時(shí)候進(jìn)行內(nèi)存分配的僅包含類變量(static變量)饰抒,而不包括實(shí)例變量肮砾,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在Java堆中;其次袋坑,這里所說(shuō)的初始值他那個(gè)場(chǎng)情況下是數(shù)據(jù)類型的零值仗处。
public static int number= 1;
public static final int numberFinal= 123;
上面例子中number
在準(zhǔn)備階段后的初始值為0而不是1,因?yàn)檫@個(gè)時(shí)候尚未開(kāi)始執(zhí)行仍和Java方法枣宫,而把number
賦值為1的putstatic
指令時(shí)程序被編譯后婆誓,存放于類構(gòu)造器<clinit>()
方法之中,所以把number
賦值為1的動(dòng)作將在初始化階段才會(huì)執(zhí)行也颤。
但是在特殊情況下洋幻,如果類字段的字段屬性表中存在ConstantValue
屬性(被final修飾),那在準(zhǔn)備階段變量numberFinal
就會(huì)被初始化為指定的值翅娶。編譯時(shí)Javac將會(huì)為numberFinal
生成ConstantValue
屬性文留,在準(zhǔn)備階段虛擬機(jī)就會(huì)根據(jù)ConstantValue
的設(shè)置將值設(shè)為123。
-
解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程竭沫,符號(hào)引用在JVM筆記:Java虛擬機(jī)的常量池提到過(guò)很多次了燥翅,在Class文件中他以
CONSTANT_Class_info、CONSTANT_Fieldref_info蜕提、CONSTANT_Methodref_info
等類型的常量出現(xiàn)森书,那解析階段中所說(shuō)的直接引用和符號(hào)引用又有什么關(guān)聯(lián)呢?符號(hào)引用(SymbolicReferences):符號(hào)引用以一組符號(hào)來(lái)描述所引用的目標(biāo)谎势,符號(hào)可以是任何形式的字面量凛膏,只要使用時(shí)能夠無(wú)歧義地定位到目標(biāo)即可。但是引用的目標(biāo)并不一定已經(jīng)加載到內(nèi)存中脏榆,它在很多情況下類似一個(gè)占位符译柏,表示將來(lái)需要指向這么一個(gè)內(nèi)容,然后在后續(xù)階段將其替換為直接引用姐霍。各種虛擬機(jī)所能接受的符號(hào)引用必須是一致的鄙麦,沒(méi)因?yàn)榉?hào)引用的字面量形式明確定義在Java虛擬機(jī)規(guī)范的Class文件格式中典唇。
直接引用(SymbolicReferences):直接引用可以是直接指向目標(biāo)的指針、相對(duì)偏移量或一個(gè)能簡(jiǎn)介定位到目標(biāo)的句柄胯府。直接引用是和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān)的介衔,同一個(gè)符號(hào)引用在不同虛擬機(jī)實(shí)例上翻譯出來(lái)的直接引用一般不會(huì)相同。如果有了直接引用骂因,那引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在炎咖。
虛擬機(jī)規(guī)范中并未規(guī)定解析階段發(fā)生的具體時(shí)間,只要求了在執(zhí)行
anewarray寒波、multianewarray乘盼、checkcast、getfield俄烁、getstatic绸栅、instanceof、invoke(dynamic,interfance,special,static,virtual)页屠、ldc试幽、ldc_w缅糟、new、putfield、putstatic
這16個(gè)字節(jié)碼之前腐晾,先對(duì)他們所使用的符號(hào)引用進(jìn)行解析磁餐。所以虛擬機(jī)實(shí)現(xiàn)可以根據(jù)需要來(lái)判斷到底是在類被加載器加載時(shí)就對(duì)常量池中的符號(hào)引用進(jìn)行解析句惯,還是等到一個(gè)符號(hào)引用將要被使用前才去解析它拍嵌。除了
invokedynamic
指令以外,虛擬機(jī)實(shí)現(xiàn)了對(duì)第一次解析的結(jié)果進(jìn)行緩存潜索,在運(yùn)行時(shí)常量池中記錄直接引用栈幸,并把常量標(biāo)識(shí)為已解析狀態(tài),從而避免解析動(dòng)作重復(fù)帮辟,如果一個(gè)符號(hào)引用解析成功或失敗速址,那么后續(xù)對(duì)其的引用解析也應(yīng)該收到成功或者異常告知。對(duì)于
invokedynamic
指令由驹,當(dāng)碰到某個(gè)前面已經(jīng)由invokedynamic
指令觸發(fā)過(guò)解析的符號(hào)引用時(shí)芍锚,并不意味著這個(gè)解析結(jié)果對(duì)于其他invokedynamic
指令也同樣生效。因?yàn)?code>invokedynamic指令的目的本來(lái)就是用于動(dòng)態(tài)語(yǔ)言支持蔓榄,它所對(duì)應(yīng)的引用稱為動(dòng)態(tài)調(diào)用點(diǎn)限定符并炮,這里動(dòng)態(tài)的含義就是必須等到程序運(yùn)行到這條指令的時(shí)候,解析動(dòng)作才能進(jìn)行甥郑。相對(duì)的逃魄,其余可觸發(fā)解析的指令都是靜態(tài)的,即可以在剛剛完成加載階段澜搅,還沒(méi)有開(kāi)始執(zhí)行代碼時(shí)就開(kāi)始進(jìn)行解析伍俘。解析動(dòng)作主要針對(duì)類或接口邪锌、字段、類方法癌瘾、接口方法觅丰、方法類型、方法句柄和調(diào)用點(diǎn)限定符7類符號(hào)引用進(jìn)行妨退,這里主要介紹前面4種妇萄,后面三種與JDK新增的動(dòng)態(tài)語(yǔ)言支持息息相關(guān),暫時(shí)這里不多做贅述咬荷,前面三種分別對(duì)于常量池的
CONSTANT_(Class冠句、Fieldref、Methodref幸乒、InterfaceMethodref)_info
懦底。1 . 類或接口的解析
假設(shè)在類
W
要把一個(gè)從未解析過(guò)的符號(hào)引用N
解析為一個(gè)類或接口O
的直接引用,那虛擬機(jī)完成整個(gè)過(guò)程主要分為以下三個(gè)步驟逝变。如果
O
不是一個(gè)數(shù)組類型,那虛擬機(jī)將會(huì)把代表N的全限定名傳遞給W的類加載器中去加載這個(gè)類O
奋构。在加載過(guò)程中壳影,由于元數(shù)據(jù)驗(yàn)證,字節(jié)碼驗(yàn)證的需要弥臼,有可能觸發(fā)其他相關(guān)類的加載動(dòng)作宴咧,一旦這個(gè)加載過(guò)程出現(xiàn)了異常,解析過(guò)程就宣告失敗径缅。如果
O
是一個(gè)數(shù)組類型掺栅,并且數(shù)組類型為對(duì)象(描述符為[Lxxx/xxx),那將會(huì)按照上面的規(guī)則加載數(shù)組元素類型纳猪,如果N的描述符如前面鎖假設(shè)的形式氧卧,那么就會(huì)加載該元素類型的對(duì)象,接著由虛擬機(jī)生成一個(gè)代表此數(shù)組維度和元素的數(shù)組對(duì)象氏堤。如果上面兩步?jīng)]有出現(xiàn)異常沙绝,那么在
c
虛擬機(jī)中實(shí)際上已經(jīng)成為一個(gè)有效的類或接口了,但在解析完成前還要進(jìn)行符號(hào)引用驗(yàn)證鼠锈,確認(rèn)W
是否具備對(duì)O
的訪問(wèn)權(quán)限闪檬,如果發(fā)現(xiàn)不具備訪問(wèn)權(quán)限,將拋出IlleagalAccessError
異常购笆。
2 . 字段解析
解析一個(gè)未被解析過(guò)得字段符號(hào)引用粗悯,首先將會(huì)對(duì)字段表內(nèi)
class_index
項(xiàng)中索引的CONSTANT_Class_info
符號(hào)引用進(jìn)行解析,也就是字段所屬的類和接口的符號(hào)引用同欠,也就是說(shuō)样傍,欲解字段横缔,必先解其所在類。解析完類后铭乾,如果類本身包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)匹配的字段剪廉,則直接返回該字段的直接引用
如果該類實(shí)現(xiàn)了接口,將會(huì)按照繼承關(guān)系遞歸搜索各個(gè)接口和他的父接口炕檩,如果接口中包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)匹配的字段斗蒋,則直接返回該字段的直接引用。
如果該類不是Object的話笛质,將會(huì)按照繼承關(guān)系遞歸搜索其父類泉沾,如果父類中包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)匹配的字段,則直接返回該字段的直接引用妇押。
如果以上步驟都失敗跷究,那么拋出
NoSuchFieldError
異常。同樣的如果不具備對(duì)返回的字段引用的訪問(wèn)權(quán)限敲霍,拋出
IlleagalAccessError
異常俊马。如果一個(gè)同名字段同時(shí)出現(xiàn)在類的接口和父類中,或者在自己父類的多個(gè)接口中出現(xiàn)肩杈,那么編譯器將可能拒絕編譯柴我。
3 . 類方法解析
類方法解析第一個(gè)步驟和字段解析一樣,也需要先解析出該方法所在的類扩然。然后按照下面步驟進(jìn)行后續(xù)的類方法搜索艘儒。
1)類方法和接口方法符號(hào)引用的常量類型定義是分開(kāi)的(一個(gè)是Methodref,一個(gè)是InterfaceMethodref),如果類方法表中發(fā)現(xiàn)索引的是一個(gè)接口夫偶,那么會(huì)拋出
IncompatibleClassChangeError
異常界睁。2)如果通過(guò)第一步,接著在類中查找是否包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)匹配的方法兵拢,則直接返回該方法的直接引用翻斟。
3)否則,在類的父類中遞歸查找是否包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)匹配的方法说铃,則直接返回該方法的直接引用杨赤。
4)否則,在類實(shí)現(xiàn)的接口列表和他們的父接口中遞歸查找是否包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)匹配的方法截汪,如果存在疾牲,說(shuō)明該類是一個(gè)抽象類(如果不是抽象類,該類中會(huì)查找到這個(gè)方法)衙解,這時(shí)候拋出
AbstractMethodError
異常阳柔。5)以上步驟都不行,拋出
NoSuchMethodError
異常蚓峦。6)同樣的如果不具備對(duì)返回的方法引用的訪問(wèn)權(quán)限舌剂,拋出
IlleagalAccessError
異常济锄。
4 . 類方法解析
老樣子,接口方法也需要先解析出接口方法表
class_info
想中索引的方法所屬的類或接口的符號(hào)引用霍转。然后按照下面步驟進(jìn)行后續(xù)的接口方法搜索荐绝。1)與類方法解析相反,如果在接口方法表中發(fā)現(xiàn)該接口所對(duì)應(yīng)的是一個(gè)類而不是接口避消,拋出
IncompatibleClassChangeError
異常低滩。2)如果通過(guò)第一步,接著在接口中查找是否包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)匹配的方法岩喷,則直接返回該方法的直接引用恕沫。
3)否則,在接口的父接口中遞歸查找纱意,直到Object類為止婶溯,看是否包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)匹配的方法,則直接返回該方法的直接引用偷霉。
4)以上步驟都不行迄委,拋出
NoSuchMethodError
異常。5)因?yàn)榻涌诜椒J(rèn)都是public的沒(méi)所以不存在訪問(wèn)權(quán)限类少,所以接口方法不會(huì)拋出
IlleagalAccessError
異常叙身。
-
初始化
類初始化時(shí)類加載過(guò)程的最后一步,前面的類加載過(guò)程中瞒滴,除了在加載階段用戶應(yīng)用程序可以通過(guò)自定義類加載器參與之外曲梗,其余動(dòng)作完全由虛擬機(jī)主導(dǎo)和控制赞警。到了初始化階段妓忍,才真正開(kāi)始執(zhí)行類中定義的Java程序代碼(或者說(shuō)字節(jié)碼)愧旦。
在準(zhǔn)備階段世剖,變量已經(jīng)賦值過(guò)一次系統(tǒng)要求的初始值,而在初始化階段峦睡,則根據(jù)程序制定的計(jì)劃去初始化類變量和其他資源翎苫,從另一個(gè)角度來(lái)表達(dá):初始化階段是指向類構(gòu)造器
<clinit>()
方法的過(guò)程权埠。<clinit>()
方法是由編譯器自動(dòng)手機(jī)類中所有的類變量(static變量)的賦值動(dòng)作和靜態(tài)語(yǔ)句塊中的語(yǔ)句合并產(chǎn)生的,編譯器收集的順序是由語(yǔ)句在源文件中出現(xiàn)的順序所決定的,靜態(tài)語(yǔ)句塊中只能訪問(wèn)到定義在靜態(tài)語(yǔ)句塊之前的變量嗎煎谍,定義在它之后的變量攘蔽,在前面的靜態(tài)語(yǔ)句塊可以賦值,但是不能訪問(wèn)呐粘。
public class Parent {
static {
a=2;
System.out.println("Parent init"+a);
}
public static int a = 1;
}
上面代碼中可以在代碼塊中對(duì)a進(jìn)行賦值满俗,但是沒(méi)啥作用,因?yàn)闀?huì)被后面的a重新賦值為1事哭,而且代碼塊內(nèi)不能調(diào)用下面的類變量漫雷,會(huì)顯示illeagal forward reference
錯(cuò)誤
<clinit>()
方法與類的構(gòu)造方法,也就是實(shí)例構(gòu)造器 <init>()
不同鳍咱,它不需要顯示地調(diào)用它父類構(gòu)造器降盹,虛擬機(jī)會(huì)保證在子類的 <clinit>()
方法執(zhí)行之前,父類的 <clinit>()
方法已經(jīng)執(zhí)行完畢谤辜,也就是說(shuō)蓄坏,父類中定義的靜態(tài)語(yǔ)句塊要由于子類的變量賦值操作,因此在虛擬機(jī)中第一個(gè)被執(zhí)行的 <clinit>()
方法的類肯定是Object丑念。
下面例子中輸出的結(jié)果就是2涡戳,因?yàn)楦割惖撵o態(tài)賦值操作比子類先執(zhí)行
public class Parent {
public static int a = 1;
static {
a=2;
}
}
public class Son extends Parent{
public static int b=a;
}
public static void main(String[] args) {
System.out.println("args = [" + Son.b + "]");
}
<clinit>()
方法不是必須的,如果一個(gè)類中沒(méi)有靜態(tài)語(yǔ)句塊脯倚,也沒(méi)有對(duì)類變量的賦值操作渔彰,那么編譯器可以不為這個(gè)類生成 <clinit>()
方法。
接口中不能使用靜態(tài)語(yǔ)句塊推正,但仍然有變量初始化的賦值操作恍涂,因此接口和類一樣都會(huì)生成 <clinit>()
方法方法,但接口與類不同的是植榕,魔之心接口的 <clinit>()
方法不需要先執(zhí)行父接口的 <clinit>()
方法再沧,只有當(dāng)父接口中定義的變量使用時(shí),父接口才會(huì)初始化尊残,另外炒瘸,接口的實(shí)現(xiàn)類在初始化時(shí)也不會(huì)執(zhí)行接口的 <clinit>()
方法。
虛擬機(jī)會(huì)保證一個(gè)類的 <clinit>()
方法在多線程環(huán)境中被正確的加鎖寝衫,用不顷扩,如果多個(gè)線程同時(shí)去初始化一個(gè)類,那么只有一個(gè)線程回去執(zhí)行這個(gè)類 <clinit>()
方法慰毅,其他線程都需要阻塞等待隘截,這也是靜態(tài)單例實(shí)現(xiàn)的原理。
-
總結(jié)
本文內(nèi)容來(lái)自于《深入Java虛擬機(jī)》,感興趣的朋友可以入這本書(shū)看看技俐。