深入理解 Java 虛擬機(五)類加載的時機

概述
虛擬機把描述類的數據從 Class 文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的 Java 類型,這就是虛擬機的類加載機制。

與 C++ 等需要進行鏈接工作的語言不同,在 Java 裏面,類的加載、連接和初始化過程都是在程序運行期間完成的,雖然這會帶來一些性能開銷,但會爲 Java 應用程序提供高度的靈活性,Java 天生可以動態擴展的語言特性就是依賴運行期動態加載和動態連接這個特點實現的。例如,應用程序可以在運行時再指定其實際的實現類;用戶可以通過 Java 預定義的和自定義的類加載器,讓一個本地的應用程序可以在運行時從網絡或其它地方加載一個二進制流作爲程序代碼的一部分,這種組裝應用程序的方式目前已經廣泛應用於 Java 程序之中,包括最基礎的 Applet、JSP 和相對複雜的 OSGi 技術。

類加載的時機
類從被加載到虛擬機內存中開始,到卸載出內存爲止,經過的生命週期包括:加載、驗證、準備、解析、初始化、使用、卸載,其中驗證、準備、解析三部分統稱爲連接,這七個階段的開始順序如圖所示:

類的加載過程

加載、驗證、準備、初始化和卸載這 5 個階段的開始順序是確定的,但解析階段則不一定,它在某些情況下可以在初始化階段之後再開始,這是爲了支持 Java 的運行時綁定。注意,這裏說的是 “開始” 順序,而不是 “進行” 順序,因爲這些階段通常是互相交叉地混合式進行的。
https://weheartit.com/5QkVAOpNOjUHk
https://weheartit.com/UUVryii4I2Q9
https://weheartit.com/uYjLBGRUfWqD

Java 虛擬機規範沒有強制約束開始類加載過程的第一階段,但對初始化階段則明確規定有且只有 5 種情況必須立即對類進行初始化:

1) 遇到 new、getstatic、putstatic 或 invokestatic 這 4 條指令時,如果類沒有進行過初始化,則必須觸發其初始化。生成這 4 條指令的常見 Java 代碼場景是:使用 new 關鍵字實例化對象、讀取或者設置一個 static 變量(被 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 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);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
輸出:

SuperClass init
123
1
2
2) 通過數組定義來引用類,不會觸發此類的初始化

public class NotInitialization {

public static void main(String[] args) {
    SuperClass[] sca = new SuperClass[10];
}

}
1
2
3
4
5
6
7
輸出:無

雖然這裏未觸發 (package_name).SuperClass 的初始化階段,但這裏觸發了另一個名爲 “[L(package_name).SuperClass” 的(數組)類的初始化,它是由虛擬機自動生成的,直接繼承 Object 的子類,創建動作由字節碼指令 newarray 觸發。這個類代表了元素類型爲 SuperClass 的一維數組,數組應有的屬性和方法都實現在這個類裏。Java 中對數組的訪問比 C++/C 相對安全就是因爲這個類封裝了數組元素的訪問方法。

3) 常量在編譯階段會存入調用類的常量池中,本質上沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化

public class ConstClass {

static {
    System.out.println("ConstClass init!");
}

public static final String HELLOWORLD = "hello world"; // 注意和 1) 不同的是,這裏使用了 final 進行修飾,屬於常量

}

public class NotInitialization {

public static void main(String[] args) {
    System.out.println(ConstClass.HELLOWORLD);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
輸出:

hello world
1
雖然源碼中引用了 ConstClass 類中的常量 HELLOWORLD,但其實在編譯階段,經過常量傳播優化,已經將此常量的值 “hello wrodld” 存儲到了 NotInitialization 類的常量池中,以後 NotInitialization 對常量 HELLOWORLD 的引用實際上都會被轉化爲對 NotInitialization 類自身對常量池的引用。

接口的加載過程與普通類有所不同:

1) 接口不能使用 static 語句塊, 但編譯期仍然會爲接口生成 < clinit > 方法,用於初始化接口所定義的成員變量

2) 當一個普通類在初始化時,要求其父類都已經初始化,但一個接口在初始化時,不要求其父接口全部完成初始化,只有在真正用到父接口的時候纔會初始化

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章