JVM學習——虛擬機類加載機制

JVM學習——虛擬機類加載機制

1 概述

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

與那些在編譯時需要進行連接工作的語言不同,在Java語言裏面,類型的加載、連接和初始化過程都是在程序運行期間完成的。

2 類加載的時機

2.1 類的生命週期

從被虛擬機加載到內存開始,到卸載出內存爲止,整個生命週期:

加載、驗證、準備、初始化、卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班的開始(互相交叉的運行),但是解析階段則不一定:可以在初始化階段之後再進行(動態綁定)

2.2 類進行初始化的5種情況(主動引用)

  1. 遇到new、getstatic(讀取靜態字段已在編譯期把結果放入常量池的靜態字段除外)、putstatic(設置靜態字段)或invokestatic(調用一個類的靜態方法)這四條字節碼指令時,如果沒有進行過初始化,則初始化。
  2. 對類進行反射調用的時候
  3. 當初始化一個類的時候,如果其父類還沒有進行初始化,則先觸發父類的初始化(如果是接口,並不要求其父接口全部完成了初始化,只有在使用的時候纔會被初始化)
  4. 虛擬機啓動時,用戶指定的執行主類(main方法),虛擬機會先初始化主類、
  5. 當使用動態語言支持時,如果java.lang.invoke.MethodHandle實例最後的解析結果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,方法句柄所對應的類沒有初始化,則觸發其初始化

2.3 舉個栗子(被動引用)

例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 ExampleTest {

    public static void main(String[] args) {
        System.out.println(SubClass.value);
        //輸出結果爲
        //superClass init
        //123
    }
}

對於靜態字段,只有直接定義這個字段的類纔會被初始化,因此通過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。

例2:通過數組定義使用類,不會觸發此類的初始化
public class Example2Test {

    public static void main(String[] args) {
        SuperClass[] superClasses = new SuperClass[10];//沒有輸出
    }
}

沒有輸出說明並沒有觸發xxx.xxx.SuperClass的初始化階段(也就是沒有執行()方法)。但是卻觸了另一個類的初始化,這個類就是[Lxxx.xxx.SuperClass,這是由虛擬機自動生成的,繼承java.lang.Object類的子類,創建動作由字節碼指令newarray觸發。這個類代表了SuperClass的一維數組,數組中應有的屬性和方法都實現在這個類中。

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

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

    public static final String HELLO = "hello";
}
public class Example3Test {
    public static void main(String[] args) {
        System.out.println(ConstantClass.HELLO);
        //輸出結果爲:hello
    }
}

沒有輸出"Constant init",這是因爲在編譯階段通過常量優化,將常量的值放入了Example3Test類的常量池中,以後對該常量的訪問都轉化爲自身對常量池的引用了。實際上Example3Test的Class文件之中並沒有ConstantClass類的符號引用入口,這兩個類在編譯成Class文件後就不存在任何聯繫

3 類加載的過程

3.1 加載

在加載階段,虛擬機需要完成的三件事:

  1. 通過一個類的全限定名類獲取定義此類的二進制流
  2. 將這個字節流代表的靜態存儲結構轉化爲方法區運行時數據結構
  3. 在內存中生成一個代表這個類的java.lang.Class對象(HotSpot中存放在方法區),作爲方法區這個類的各種數據的訪問入口

3.2 驗證

3.2.1 文件格式驗證

一部分驗證點:

  • 是否以魔數0xCAFEBABE開頭
  • 主、次版本號是否在虛擬機處理範圍之內
  • 常量池中的常量是否有不被支持的常量類型(檢查常量的tag標誌)

只有通過了這個階段的驗證後,字節流纔會進入內存的方法區中進行存儲,所以後面的三個驗證階段全部是基於方法區的存儲結構進行的,不會直接操作字節碼

3.2.2 元數據驗證(語義分析,是否符合Java語言規範)
3.2.3 字節碼驗證

一部分驗證點:

  • 保證任意時刻操作數棧的數據類型與指令代碼序列都能配合工作,例如不會出現類似這樣的情況:在操作數棧中放置一個int類型的數據,使用時卻按照long類型來加載入本地變量表
  • 保證跳轉指令不會跳轉到方法體以外的字節碼指令上
3.2.4 符號引用驗證(發生在虛擬機將符號引用轉化爲直接引用時,解析階段發生。可以看作是對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗)

3.3 準備

準備階段是正式爲類變量(static修飾的變量,實例變量將會在對象實例化時分配在java堆中)分配類存並設置類變量初始值的階段(初始值通常是數據類型的零值,特殊情況是被加上final修飾的數據初始值就是指定的值,如public static final int value = 123),變量所使用的內存在方法區中進行分配。

3.4 解析

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程

  • 符號引用:符號引用以一組符號來描述所引用的目標,引用的目標不一定已經加載到內存中。
  • 直接引用:直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄

注意:虛擬機對解析進行緩存,對符號引用的第一次結果進行緩存(在運行時常量池中記錄直接引用,並把常量標爲已解析狀態),從而避免重複解析(對invokeddynamic指令,上面規則不成立,必須等到代碼實際運行到這條指令)

3.4.1 類或接口的解析
3.4.2 字段解析
3.4.3 類方法解析
3.4.4 接口方法解析

3.5 初始化

初始化階段是類加載過程的最後一步,根據程序員通過程序制定的主觀計劃去初始化類變量和其他資源(執行()方法的過程)

3.5.1 <clinit>()方法
  • <clinit>()方法是有編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊中的語句合併產生的,編譯器的收集順序源文件的順序(靜態語句塊只能訪問定義在靜態語句塊前的變量,後面的變量可以賦值但是不能訪問)
  • <clinit>()方法與類的構造函數不同,不用顯示的調用,虛擬機保證在子類的方法執行前,父類的方法已經執行完畢,最先的肯定是object
  • 虛擬機會保證一個類的<clinit>()方法在多線程環境中會被正確的加鎖、同步,如果多個線程同時初始化一個類,只會有一個類執行相應的方法
3.5.2 不被初始化的例子
  • 通過子類引用父類的靜態字段,子類不會被初始化
  • 通過數組定義來引用類
  • 調用類的常量
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章