1. 類的生命週期過程
從類被加載到JVM內存中開始到被卸載出內存爲止,生命週期會經過以下7過程:
- 加載、驗證、準備、初始化和卸載這五個階段的順序是確定的。
- 解析階段不一定按上圖順序進行,在某些情況下解析階段可以在初始化之後進行(爲了支持Java的運行時綁定特性,也稱爲動態綁定)
2. 六種必須立即進行初始化的操作
”加載“過程什麼時候開始並沒有明確的規定,但是什麼時候必須進行類初始化有着嚴格的規定,僅在以下6種情況中需要立即進行類初始化操作(初始化之前的必要操作加載、驗證、準備肯定也要進行)
這6種場景稱爲對一個類型進行“主動引用”,除此之外所有引用類型方式都不會觸發初始化
- 遇到new、getstatic、putstatic或者invokestatic這四條字節碼指令時,如果類型沒有進行過初始化,則需要先觸發其初始化階段。能夠生成這四條指令的典型Java代碼場景有:
- 使用new關鍵字實例化對象的時候
- 讀取或者設置一個類型的靜態字段時(被final修飾,已在編譯期將結果放入常量池的靜態字段除外,注意是除外除外除外!)
- 調用一個類型的靜態方法的時候
- 使用java.lang.reflect包的方法對類型進行反射調用的時候,如果沒有進行過初始化,則需要先進行初始化
- 當初始化類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化
- 當JVM啓動的時候,用戶需要指定一個要執行的主類(包含main方法的類)虛擬機會先初始化這個類
- 當使用JDK7新加入的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果爲REF_getstatic、REF_putstatic、REF_invokeStatic、REF_newInvokeSpecial四種類型的方法句柄,並且這個方法句柄對應的類沒有進行過初始化,則需要先觸發其初始化
- 當一個接口中定義了JDK8新加入的默認方法(被default關鍵字修飾的接口方法)時,如果有這個接口的實現類發生了初始化,則該接口要在其之前被初始化
3. 不會觸發 類初始化 的情況
以上的6種場景的行爲成爲對一個類型進行主動引用,除此之外的所有引用類型都不會觸發初始化,稱爲被動引用。
列舉三種不會觸發類初始化的場景:
- Demo1: 通過子類引用父類的靜態字段,不會導致子類初始化
class A{
public static String super_str = "super_str";
static {
System.out.println("A初始化...");
}
A(){
System.out.println("A被創建...");
}
}
class B extends A{
public static String child_str = "child_str";
static {
System.out.println("B初始化...");
}
B(){
System.out.println("B被創建...");
}
}
public class Demo1 {
public static void main(String[] args) {
// 通過子類引用父類中的靜態字段
System.out.println(B.super_str);
}
}
輸出結果及其分析:
A初始化...
super_str
分析:對於靜態字段,只有直接定義這個字段的類纔會被初始化,
因此通過其子類來引用父類中定義的靜態字只會觸發父類的初始化
除此之外:是否要觸發子類的加載和驗證階段:
在HotSpot VM通過-XX:+TraceClassLoading參數觀察到此操作是 會觸發的
- Demo2:
class A{
static {
System.out.println("A初始化...");
}
}
public class Demo2 {
public static void main(String[] args) {
// 使用數組定義來引用類,不會觸發類的初始化
A[] aa = new A[10];
}
}
輸出結果及其分析:
什麼也沒輸出...
分析:沒有任何輸出說明沒有觸發A類的初始化
(以下內容暫未實驗,暫時轉述書本上的內容)
但是會觸發一個名爲"[Lorg.feinxsoft.classloading.A"的類初始化階段,對於用戶代碼來說,
這不是一個合法的類型名稱,它是一個由JVM自動生成的,直接繼承與java.lang.Object的子類,
創建動作由字節碼指令newarray觸發。
這個類代表了一個元素類型爲org.feinxsoft.classloading.A的一維數組,
數組中應有的屬性和方法都被實現在這個類裏面。
- Demo3:
class A{
// 和Demo1相比僅僅多了final
public static final String super_str = "super_str";
static {
System.out.println("A初始化...");
}
A(){
System.out.println("A被創建...");
}
}
public class Demo3 {
public static void main(String[] args) {
// // 使用類中的final常量
System.out.println(A.super_str);
}
}
輸出結果及其分析:
super_str
分析:常量在編譯階段會被存入調用類的常量值中,本質上沒有直接引用到定義常量的類,
因此不會觸發定義常量的類的初始化。
在編譯階段通過常量傳播優化,已經將常量的值"super_str"直接存儲在Demo3的類的常量池中,
以後Demo3中對常量super_str的引用實際上都被轉化爲Demo3類對自身常量池的引用了。
實際上Demo3的Class文件中並沒有A類的符號引用入口,
這兩個類在編譯成Class文件之後就沒有任何聯繫了
4. 有關接口
接口也有初始化過程,在前面的6中必須進行初始化的操作中,有一點不同:
- 當一個類在初始化的時候,要求其父類全部已經初始化過了,否則先對父類進行初始化
- 當一個接口在進行初始化的時候,不要求其父類接口全部完成了初始化,只有在真正使用到父接口的時候纔會初始化,比如要使用父接口中定義的常量的時候。
參考:
https://www.jianshu.com/p/7ecbb90478ef
https://blog.csdn.net/weixin_34211761/article/details/88030782