類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括:加載、驗證、准備、解析、初始化、使用和卸載七個階段。其中驗證、准備、解析3個部分統稱為連接。類加載的過程包括了加載、驗證、准備、解析、初始化五個階段。
加載、驗證、准備、初始化和卸載這5個階段的順序時確定的,類的加載過程必須按照這種順序按部就班的開始,而解析階段則不一定,它在某些情況下可以在初始化階段之后開始,這是為了支持Java語言的運行時綁定(也成為動態綁定或晚期綁定)。另外注意這里的幾個階段是按順序開始,而不是按順序進行或完成,因為這些階段通常都是互相交叉地混合進行的,通常在一個階段執行的過程中調用或激活另一個階段。
類初始化是類加載過程的最后一個階段,到初始化階段,才真正開始執行類中的Java程序代碼。虛擬機規范嚴格規定了有且只有5種情況必須立即對類進行初始化:
- 第一種:遇到new、getstatic、putstatic、invokestatic這四條字節碼指令時,如果類還沒有進行過初始化,則需要先觸發其初始化。生成這四條指令最常見的Java代碼場景是:使用new關鍵字實例化對象時、讀取或設置一個類的靜態字段(static)時(被static修飾又被final修飾的,已在編譯期把結果放入常量池的靜態字段除外)、以及調用一個類的靜態方法時。
- 第二種:使用Java.lang.refect包的方法對類進行反射調用時,如果類還沒有進行過初始化,則需要先觸發其初始化。
- 第三種:當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先觸發其父類的初始化。
- 第四種:當虛擬機啟動時,用戶需要指定一個要執行的主類,虛擬機會先執行該主類。
- 第五種:當使用JDK1.5支持時,如果一個java.langl.incoke.MethodHandle實例最后的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。
虛擬機規定有且只有這5種情況才會觸發類的初始化,這5中場景中的行為稱為對一個類進行主動引用,除此之外所有引用類的方式都不會觸發其初始化,稱為被動引用。下面舉一些例子來說明被動引用。
1、通過子類引用父類中的靜態字段,這時對子類的引用為被動引用,因此不會初始化子類,只會初始化父類
package org.wrh.classupload;
/* * 通過子類引用父類的靜態字段,不會導致子類初始化 * */ public class TestClassDemo01 { public static void main(String[] args) { // TODO Auto-generated method stub System.out.println(SubClass.i);//子類引用父類的靜態字段 } } class SuperClass{ public static int i=3;//父類的static字段 static{//當此類在虛擬機中初始化的時候,此static塊將會被執行 System.out.println("SuperClass init"); } } class SubClass extends SuperClass{ static{//當此類在虛擬機中初始化的時候,此static塊將會被執行 System.out.println("SubClass init"); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
程序運行結果如下:
SuperClass init
3
即只輸出了“SuperClass init”,而沒有輸出“SubClass init”。
結論:對於靜態字段,只有直接定義這個字段的類才會被初始化,因此通過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。至於是否要觸發子類的加載和驗證,在虛擬機規范中並未明確規定,這點取決於虛擬機的具體實現。
使用new實例化對象時,會先把父類初始化,然后再初始化此類本身
package org.wrh.classupload;
/* * 通過子類引用父類的靜態字段,不會導致子類初始化 * */ public class TestClassDemo01 { public static void main(String[] args) { //System.out.println(SubClass.i); //SuperClass s=new SuperClass(); SubClass s=new SubClass(); } } class SuperClass{ public static int i=3; static{ System.out.println("SuperClass init"); } } class SubClass extends SuperClass{ static{ System.out.println("SubClass init"); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
運行結果如下:
SuperClass init
SubClass init
通過數組定義來引用類,不會觸發此類的初始化
package org.wrh.classupload;
public class TestClassDemo02 { public static void main(String[] args) { // TODO Auto-generated method stub SuperClass_1 superClass[]=new SuperClass_1[5]; } } class SuperClass_1{ static{ System.out.println("SuperClass init"); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
執行后沒有輸出任意內容,說明沒有進行任何類的初始化工作。
但是,但這段代碼里觸發了另一個名為“LLSuperClass_1”的類的初始化,它是一個由虛擬機自動生成的、直接繼承於java.lang.Object的子類,創建動作由字節碼指令newarray觸發,很明顯,這是一個對數組引用類型的初始化。
常量在編譯階段會存入調用它的類的常量池中,本質上沒有直接引用到定義該常量的類,因此不會觸發定義常量的類的初始化
package org.wrh.classupload;
public class NotInitialization { public static void main(String[] args) { System.out.println(ConstClass.VALUE); } } class ConstClass{ public static final int VALUE=3; static{ System.out.println("ConstClass init"); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
運行結果如下:
3
沒有輸出“ConstClass init”,因此可以得到當我們引用final修飾的常量時此類並沒有初始化。
雖然程序中引用了ConstClass類的常量VALUE,但是在編譯階段將此常量的值“3”存儲到了調用它的類ConstClass的常量池中,對常量Const.VALUE的引用實際上轉化為了ConstClass類對自身常量池的引用。也就是說,實際上ConstClass的Class文件之中並沒有Const類的符號引用入口,這兩個類在編譯成Class文件后就不存在任何聯系了。
接口的加載過程
接口的加載過程與類加載過程稍微有點不同,針對接口需要做一些特殊的說明:接口也有初始化過程,這點與類是一致的,上面的代碼都是用靜態語句塊”static{}“來輸出初始化信息的,而接口中不能使用”static{}“語句塊,但是編譯器仍然會為接口生成”()”類構造器,用於初始化接口中所定義的成員變量。接口與類真正有所區別的是前面講述的5種“有且只有”需要開始初始化場景中的第三種:當一個類在初始化時,要求其父類全部都已經初始化過了,但是在接口在初始化的時候,並不要求其父接口都完成了初始化,只要在真正使用到父接口的時候(如引用接口中定義的常量)才會初始化。