官术网_书友最值得收藏!

2.3 類的初始化

類可用三部曲的最后一步是類初始化。《Java虛擬機規范》的第5章對初始化流程有非常詳盡的描述,指出整個類的初始化流程有12步。

1)獲取類C的初始化鎖LC。

2)如果另外一個線程正在初始化C,那么釋放鎖LC,阻塞當前線程,直到另一個線程初始化完成。

3)如果當前線程正在初始化C,那么釋放LC。

4)如果C早已初始化,不需要做什么,那么釋放LC。

5)如果C處于錯誤的狀態,初始化不可能完成,則釋放LC并拋出NoClassDefFoundError。

6)否則,標示當前線程正在初始化C,釋放LC。然后初始化每個final static常量字段,初始化順序遵照代碼寫的順序。

7)下一步,如果C是類而不是接口,初始化父類和父接口。

8)下一步,查看C的斷言是否開啟。

9)下一步,執行類或者接口的初始化方法。

10)如果初始化完成,那么獲取鎖LC,標示C已經完全初始化,通知所有等待的線程,然后釋放LC。

11)否則,初始化一定會遇到類問題,拋出異常E。如果類E是Error或者它的子類,那么創建一個ExceptionInitializationError對象,將E作為參數,然后用該對象替代下一步的E。如果因為OutOfMemoryError原因不能創建ExceptionInitializationError實例,則使用OutOfMemoryError實例作為下一步E的替代品。

12)獲取LC,標示C為錯誤狀態,通知所有線程,然后釋放LC,以上一步的E作為本步的終止。

為了通用性和抽象性,可能《Java虛擬機規范》在語言描述方面比較學究。要想直觀了解類初始化過程,可以閱讀InstanceKlass::initialize_impl()源碼實現。HotSpot VM幾乎是按照Java虛擬機規范要求的步驟進行的,只是看起來更簡單明了。不難看出,上面步驟很多都是為了處理錯誤和異常情況,真正意義上的初始化其實是第9步,如代碼清單2-14所示:

代碼清單2-14 類初始化


void InstanceKlass::initialize_impl(TRAPS) {
    ...
    // Step 8 (虛擬機文檔的第9步對應源碼第8步,因為源碼省略了文檔第8步的處理)
    call_class_initializer(THREAD);
}
void InstanceKlass::call_class_initializer(TRAPS) {
    // 如果啟用了編譯重放則跳過初始化
    if (ReplayCompiles && ...){
        return;
    }
    // 獲取初始化方法,包裝成一個methodHandle
    methodHandle h_method(THREAD, class_initializer());
    // 調用初始化方法
    if (h_method() != NULL) {
        JavaCallArguments args; // <clinit>無參數
        JavaValue result(T_VOID);
        JavaCalls::call(&result, h_method, &args, CHECK); 
    }
}

類初始化首先會判斷是否開啟了編譯重放(Replay Compile)。使用“-XX:CompileCommand=option,ClassName::MethodName,DumpInline”可以將一個方法的編譯信息存放到文件,這樣就可以在下一次運行時使用-XX:+ReplayCompiles -XX:ReplayDataFile=file從文件讀取編譯數據,并創建編譯任務投入編譯隊列,然后進入阻塞狀態,在編譯完成后繼續執行程序。這種“第一次運行存放編譯任務→第二次運行獲取編譯任務→第二次執行編譯”的過程就是編譯重放。

編譯重放固定了編譯順序,而固定的編譯順序減少了虛擬機的不確定性,可用于JIT編譯器性能數據分析和GC性能數據分析等場景。除此之外,虛擬機參數-XX:ReplaySuppressInitializers=<val>的值還可以控制類初始化行為:

? 0:不做特殊處理;

? 1:將所有類初始化代碼視為空;

? 2:將所有應用程序類初始化代碼視為空;

? 3:允許啟動時運行類初始化代碼,但是在重放編譯時忽略它們。

處理了編譯重放后,虛擬機會調用class_initializer()函數,該函數返回當前類的<clinit>方法。類的構造函數和靜態代碼塊在虛擬機中有特殊的名字,前者是<init>,后者則是<clinit>。靜態代碼塊如代碼清單2-15所示。

代碼清單2-15 靜態代碼塊


public class ClinitTest{
    private static int k;
    private static Object obj = new Object();
    static{
        k = 12;
    }
    public static void main(String[] args){
        new ClinitTest();
    }
}

對于代碼清單2-15,Java編譯器會將靜態字段的初始化代碼也放入<clinit>,所以字段k和字段obj的賦值都是在類初始化階段完成的,也正是因為賦值操作需要真實的執行代碼,所以需要在鏈接階段提前設置解釋器入口,以便初始化代碼的執行。在確認class_initializer()返回的當前類的<clinit>方法存在后,虛擬機會將其包裝成methodHandle送入JavaCalls::call執行。

虛擬機和Java溝通的兩座橋梁是JNI和JavaCalls,Java層使用JNI進入JVM層,而JVM層使用JavaCalls進入Java層。JavaCalls可以在HotSpot VM中調用Java方法,main方法執行也是使用這種JavaCalls實現的。關于JavaCalls在第4章會詳細討論。

主站蜘蛛池模板: 桐梓县| 乌兰浩特市| 奉化市| 海门市| 谢通门县| 海阳市| 乳源| 无为县| 汶川县| 曲阳县| 岱山县| 和硕县| 科技| 大安市| 崇阳县| 霸州市| 丁青县| 平昌县| 工布江达县| 丹凤县| 屏东县| 汾阳市| 阳泉市| 西盟| 尼玛县| 织金县| 南召县| 太仓市| 中宁县| 石渠县| 涟水县| 揭东县| 北宁市| 滦平县| 西畴县| 南充市| 广丰县| 宽城| 贵定县| 盘山县| 莒南县|