引起類加載操作的行為有哪些?
類的生命周期:
類從被加載到虛擬機內存中開始,到卸載出內存結束。生命周期包括:加載、驗證、準備、解析、初始化、使用、卸載;其中驗證、準備、解析稱為連接。
加載、驗證、準備、初始化、卸載,這幾個階段的順序是確定的,類的加載過程必須按照這個順序按部就班的開始;解析階段不一定,某些情況下可以在初始化階段之后再開始。
必須立即對類進行 "初始化" 的5種情況(虛擬機規范規定):
注意:加載、驗證、準備階段必須在此之前開始
1、遇到 new、getstatic、putstatic 或 invokestatic 這 4 條字節碼指令時,如果沒有進行過初始化,需要先出發其初始化;
生成這4條指令的最常見的 java 代碼場景是:使用 new 關鍵字實例化對象的時候;讀取、設置一個類的靜態字段(被 final 修飾,已在編譯器把結果放入常量池的靜態字段除外)的時候;調用一個類的靜態方法的時候。
2、使用 java.lang.reflect 包的方法對類進行反射調用的時候,如果類沒有進行過初始化,需要先出發其初始化;
3、當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化;
4、當虛擬機啟動時,用戶需要指定一個要執行的主類(包含 main() 方法的那個類),虛擬機會先初始化這個主類;
5、當使用 JDK 1.7 的動態語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最后的解析結果 REF_getstatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。
這 5 中場景中的行為稱為對一個類的主動引用。另外,所有引用類的方式都不會觸發初始化,被稱為被動引用。
被動引用示例:
1、通過子類引用父類的靜態字段,不會導致子類初始化;
public class TestNotInit {
public static void main(String[] args) {
// 通過子類引用父類的靜態字段,不會導致子類初始化
System.out.println(SubClass.value);
}
}
class SuperClass {
public static int value = 500;
static {
System.out.println("super class init");
}
}
class SubClass extends SuperClass {
static {
System.out.println("sub class init");
}
}
輸出結果如下:
super class init
500
2、通過數組定義來引用類,不會觸發此類的初始化;
public class TestNotInit {
public static void main(String[] args) {
// 通過數組定義來引用類,不會觸發此類的初始化
SuperClass[] array = new SuperClass[10];
}
}
class SuperClass {
public static int value = 500;
static {
System.out.println("super class init");
}
}
輸出結果如下(什么都沒有輸出):
3、常量在編譯階段會存入調用類的常量池中,本質上并沒有直接引用到定義常量的類,不會觸發定義常量的類的初始化。
public class TestNotInit {
public static void main(String[] args) {
// 常量在編譯階段會存入調用類的常量池中,本質上并沒有直接引用到定義常量的類,不會觸發定義常量的類的初始化
System.out.println(SuperClass.HELLO_WORLD);
}
}
class SuperClass {
public static final String HELLO_WORLD = "hello world";
static {
System.out.println("super class init");
}
}
輸出結果如下:
1
hello world
備注:接口與類真正有區別的是 第3條:當一個接口在初始化時,并不要求其父接口全部都完成了初始化,只有在真正用到父接口的時候(引用接口中定義的常量)才會初始化。