- 深入解析Java虛擬機HotSpot
- 楊易
- 1399字
- 2021-01-07 11:18:27
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章會詳細討論。
- Mastering Concurrency Programming with Java 8
- GAE編程指南
- C#完全自學教程
- Learning AWS Lumberyard Game Development
- Data Analysis with IBM SPSS Statistics
- Hands-On C++ Game Animation Programming
- Building a Quadcopter with Arduino
- Linux操作系統基礎案例教程
- 程序是怎樣跑起來的(第3版)
- 編程與類型系統
- Flink入門與實戰
- 讓Python遇上Office:從編程入門到自動化辦公實踐
- Mastering JavaScript Promises
- Python全棧開發:數據分析
- 計算機軟件項目實訓指導