本篇來自周志明的<<深入理解java虛擬機>>
類從被加載到虛擬機內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期包括:加載(Loading)、驗證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中驗證、準(zhǔn)備、解析三個部分統(tǒng)稱為連接(Linking),這7個階段的發(fā)生順序如下圖所示:
加載、驗證、準(zhǔn)備、初始化和卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持Java語言的運行時綁定(也稱為動態(tài)綁定或晚期綁定)。注意,這里筆者寫的是按部就班地“開始”,而不是按部就班地“進(jìn)行”或“完成”,強調(diào)這點是因為這些階段通常都是互相交叉地混合式進(jìn)行的,通常會在一個階段執(zhí)行的過程中調(diào)用、激活另外一個階段。
什么情況下需要開始類加載過程的第一個階段:加載?Java虛擬機規(guī)范中并沒有進(jìn)行強制約束,這點可以交給虛擬機的具體實現(xiàn)來自由把握。但是對于初始化階段,虛擬機規(guī)范則是嚴(yán)格規(guī)定了有且只有5種情況必須立即對類進(jìn)行“初始化”(而加載、驗證、準(zhǔn)備自然需要在此之前開始):
- 遇到new、getstatic、putstatic或invokestatic這4條字節(jié)碼指令時,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關(guān)鍵字實例化對象的時候、讀取或設(shè)置一個類的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時候,以及調(diào)用一個類的靜態(tài)方法的時候。
- 使用java.lang.reflect包的方法對類進(jìn)行反射調(diào)用的時候,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。
- 當(dāng)初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化。
- 當(dāng)虛擬機啟動時,用戶需要指定一個要執(zhí)行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
- 當(dāng)使用JDK 1.7的動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結(jié)果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄所對應(yīng)的類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。
對于這5種會觸發(fā)類進(jìn)行初始化的場景,虛擬機規(guī)范中使用了一個很強烈的限定語:“有且只有”,這5種場景中的行為稱為對一個類進(jìn)行主動引用。除此之外,所有引用類的方式都不會觸發(fā)初始化,稱為被動引用。下面舉3個例子來說明何為被動引用,分別見以下代碼清單
/** * 被動使用類字段演示一: * 通過子類引用父類的靜態(tài)字段,不會導(dǎo)致子類初始化 */public class SuperClass { static { System.out.println("SuperClass init!"); } public static int value=123; } public class SubClass extends SuperClass{ static { System.out.println("SubClass init!"); }} public class NotInitialization { public static void main(String[] args) { System.out.println(SubClass.value); }}
SuperClass init!123
上述代碼運行之后,只會輸出“SuperClass init!”,而不會輸出“SubClass init!”。對于靜態(tài)字段,只有直接定義這個字段的類才會被初始化,因此通過其子類來引用父類中定義的靜態(tài)字段,只會觸發(fā)父類的初始化而不會觸發(fā)子類的初始化。至于是否要觸發(fā)子類的加載和驗證,在虛擬機規(guī)范中并未明確規(guī)定,這點取決于虛擬機的具體實現(xiàn)。對于Sun HotSpot虛擬機來說,可通過-XX:+TraceClassLoading參數(shù)觀察到此操作會導(dǎo)致子類的加載。
/** * 被動使用類字段演示二: * 通過數(shù)組定義來引用類,不會觸發(fā)此類的初始化 */public class NotInitialization { public static void main(String[] args) { SuperClass[] superClasses = new SuperClass[10]; }}
為了節(jié)省版面,這段代碼復(fù)用了第一個代碼清單中的SuperClass,運行之后發(fā)現(xiàn)沒有輸出“SuperClass init!”,說明并沒有觸發(fā)類org.fenixsoft.classloading.SuperClass的初始化階段。但是這段代碼里面觸發(fā)了另外一個名為“[Lorg.fenixsoft.classloading.SuperClass”的類的初始化階段,對于用戶代碼來說,這并不是一個合法的類名稱,它是一個由虛擬機自動生成的、直接繼承于java.lang.Object的子類,創(chuàng)建動作由字節(jié)碼指令newarray觸發(fā)。
這個類代表了一個元素類型為org.fenixsoft.classloading.SuperClass的一維數(shù)組,數(shù)組中應(yīng)有的屬性和方法(用戶可直接使用的只有被修飾為public的length屬性和clone()方法)都實現(xiàn)在這個類里。Java語言中對數(shù)組的訪問比C/C++相對安全是因為這個類封裝了數(shù)組元素的訪問方法,而C/C++直接翻譯為對數(shù)組指針的移動。在Java語言中,當(dāng)檢查到發(fā)生數(shù)組越界時會拋出java.lang.ArrayIndexOutOfBoundsException異常。
/** *常量在編譯階段會存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用到定義常量的類,因此不會觸發(fā)定義常量的類 *的初始化 **/public class ConstClass { static { System.out.println("ConstClass init!"); } public static final String HELLOWWORLD="hello world"; } /** *非主動使用類字段演示 **/public class NotInitialization { public static void main(String[] args) { System.out.println(ConstClass.HELLOWWORLD); }}
上述代碼運行之后,也沒有輸出“ConstClass init!”,這是因為雖然在Java源碼中引用了ConstClass類中的常量HELLOWORLD,但其實在編譯階段通過常量傳播優(yōu)化,已經(jīng)將此常量的值“hello world”存儲到了NotInitialization類的常量池中,以后NotInitialization對常量ConstClass.HELLOWORLD的引用實際都被轉(zhuǎn)化為NotInitialization類對自身常量池的引用了。也就是說,實際上NotInitialization的Class文件之中并沒有ConstClass類的符號引用入口,這兩個類在編譯成Class之后就不存在任何聯(lián)系了。
接口的加載過程與類加載過程稍有一些不同,針對接口需要做一些特殊說明:接口也有初始化過程,這點與類是一致的,上面的代碼都是用靜態(tài)語句塊“static{}”來輸出初始化信息的,而接口中不能使用“static{}”語句塊,但編譯器仍然會為接口生成“<clinit>()”類構(gòu)造器,用于初始化接口中所定義的成員變量。接口與類真正有所區(qū)別的是前面講述的5種“有且僅有”需要開始初始化場景中的第3種:當(dāng)一個類在初始化時,要求其父類全部都已經(jīng)初始化過了,但是一個接口在初始化時,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的時候(如引用接口中定義的常量)才會初始化。