前言
一直想好好的瞭解一下JVM,這次就來一起了解一下JVM是如何實現類的加載過程的。
原理
類加載的生命週期
1. 加載
5種類加載情況:
- 在遇到 new、putstatic、getstatic、invokestatic 字節碼指令時,如果類尚未初始化,則需要先觸發初始化。
- 對類進行反射調用時,如果類還沒有初始化,則需要先觸發初始化。
- 初始化一個類時,如果其父類還沒有初始化,則需要先初始化父類。
- 虛擬機啓動時,用於需要指定一個包含 main() 方法的主類,虛擬機會先初始化這個主類。
- 當使用 JDK 1.7 的動態語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最後的解析結果爲 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,並且這個方法句柄所對應的類還沒初始化,則需要先觸發初始化。
因爲Java類加載是不關心這個文件來自哪裏?
所有Java的對象可以來自Jar,war,網絡,CGlib,數據庫等等。
使用到對象纔會加載。
2. 驗證
- 文件格式驗證
- 元數據驗證
- 字節碼驗證
- 符號引用驗證
3. 準備
爲靜態變量分配內存、爲基礎數據類型賦初值,
如果被final修飾,在編譯的時候會給屬性添加ConstantValue屬性,準備階段直接完成賦值操作,無需賦初值。
4. 解析
間接引用轉直接引用。
間接引用:指向常量池。
直接引用:指向內存地址。
5. 初始化
執行靜態代碼段(靜態代碼塊,靜態變量):clinit
測試代碼寫了一個靜態變量b 通過字節碼文件也能清楚的看到信息。
靜態代碼的執行順序和定義的順序一致,改點可以通過案例說明。
public class JvmTest1 { public static void main(String[] args) { Test1 test1 = Test1.getTest(); System.out.println(Test1.val1); System.out.println(Test1.val2); } } class Test1{ public static int val1 ; Test1(){ val1++; val2++; } public static Test1 instance = new Test1(); public static int val2 = 1 ; // 該代碼放在在初始化代碼 public static Test1 getTest(){ return instance; } }
運行結果:
運行結果是1和1 ,造成這個結果的原因就是靜態代碼的順序執行,先創建了對象,對象中構造函數執行了, 後面再執行 public static int val2 = 1 ,值就被覆蓋回去了。
6. 使用
對象已經初始化完成,這個對象就可以再其他地方被使用。
7. 卸載
銷燬之前加載的對象信息。
Jvm裏面有一個類狀態的枚舉:
JVM類加載細節
1. jvm類加載會加鎖,防止類加載出現資源爭用的情況。
通過一個案例證明一下:
public class JvmTest3 { public static void main(String[] args) { new Thread(() -> { while(true){ new AA(); } }).start(); new Thread(() -> { while(true) { new BB(); } }).start(); } } class AA{ static { System.out.println("創建AA對象"); new BB(); } } class BB{ static { System.out.println("創建BB對象"); new AA(); } }
運行結果:
執行被鎖,無法繼續執行。
2. Jvm加載對象屬於懶加載(對象沒有使用的時候,不會去加載該對象),下面通過案例說明:
public class JvmTest2 { public static void main(String[] args) { System.out.println(B.str); } } class A { static String str = "a"; static { System.out.println("static a"); } } class B extends A { static { System.out.println("static b"); } }
運行結果:
可以明顯的看到B的static方法沒有被執行,因爲這段邏輯中,不需要是到B這個對象,哪怕B對象是A對象的子類。
總結
Jvm 類加載是核心邏輯之一,非常重要,對一個變成人員的提升也是非常的有幫助。