JVM類載入機制

吾日三省吾码發表於2024-10-04

概述

虛擬機器把描述類的資料從CLass檔案載入到記憶體,並對資料進行校驗,解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是虛擬機器的類載入機制(懶載入)。

類載入過程

載入--連線--初始化--使用--解除安裝

JVM類載入機制

1. 載入(Loading)

  • 通過一個類的全限定名來獲取定義此類的二進位制流
  • 將這個位元組流鎖代表的靜態儲存結構轉化為方法區的執行時資料結構
  • 在記憶體中生成一個代表這個類的Class物件,作為方法區這個類的各種資料的訪問入口。

1.1 載入源

(1)從本地系統直接載入
(2)通過網路下載.class檔案
(3)從zip,jar等歸檔檔案中載入.class檔案
(4)從專有資料庫中提取.class檔案
(5)將Java原始檔動態編譯為.class檔案(伺服器)
......

2. 連線

2.1 驗證

驗證是連線的第一步,這一階段的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全

2.1.1 檔案格式驗證

(1)是否以魔數0xCAFEBABE開頭。
(2)主、次版本號是否在當前虛擬機器處理範圍之內。
(3)常量池的常量中是否有不被支援的常量型別(檢查常量tag標誌)。
(4)指向常量的各種索引值中是否有指向不存在的常量或不符合型別的常量。
(5)CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的資料。
(6)Class檔案中各個部分及檔案本身是否有被刪除的或附加的其他資訊。
......

2.1.2 後設資料驗證

(1)這個類是否有父類(除了java.lang.Object之外,所有類都應當有父類)。 (2)這個類是否繼承了不允許被繼承的類(被final修飾的類)。 (3)如果這個類不是抽象類,是否實現了其父類或介面之中所要求實現的所有方法。 (4)類中的欄位、方法是否與父類產生矛盾(例如覆蓋了父類的final欄位,或者出現不符合規則的方法過載,例如方法引數都一致,但返回值型別卻不同等等)。 ......

2.1.3 位元組碼驗證

主要目的是通過資料流和控制流分析,確定程式語義是合法的、符合邏輯的。這個階段將對類的方法體進行校驗分析,保證被校驗類的方法在執行時不會產生危害虛擬機器安全的事件,例如:
(1)保證任意時刻運算元棧的資料型別與指令程式碼序列都能配合工作,例如不會出現類似這樣的情況:在運算元棧放置了一個int型別的資料,使用時卻按long型別來載入入本地變數表中。
(2)保證跳轉指令不會跳轉到方法體以外的位元組碼指令上。
(3)保證方法體中的型別轉換是有效的,例如可以把一個子類物件賦值給父類資料型別,但是把父類物件賦值給子類資料型別,甚至把物件賦值給與它毫無繼承關係、完全不相干的一個資料型別,則是危險不合法的。
......

2.1.4 符號引用驗證

符號引用驗證可以看作是類對自身以外(常量池中的各種符號引用)的資訊進行匹配性校驗,通常需要校驗以下內容:
(1)符號引用中通過字串描述的全限定名是否能夠找到對應的類。
(2)在指定類中是否存在符合方法的欄位描述符以及簡單名稱所描述的方法和欄位。
(3)符號引用中的類、欄位、方法的訪問性(private、protected、public、default)是否可被當前類訪問。
......

2.2 準備

為類變數分配記憶體並設定變數的初始值,這些變數使用的記憶體都將在方法區中進行分配。

整體型別是:
  • byte,其值為8位有符號二進位制補碼整數,其預設值為零

  • short,其值為16位有符號二進位制補碼整數,其預設值為零

  • int,其值為32位有符號二進位制補碼整數,其預設值為零

  • long,其值為64位帶符號的二進位制補碼整數,其預設值為零

  • char,其值為16位無符號整數,表示基本多語言平面中的Unicode程式碼點,使用UTF-16編碼,其預設值為空程式碼點('\u0000'

浮點型別是:
  • float,其值是浮點值集的元素,或者,如果支援,則為float-extended-exponent值集,其預設值為正零

  • double,其值是double值集的元素,或者,如果支援,則為double-extended-exponent值集,其預設值為正零

所述的值boolean 型別編碼的真值truefalse,並且預設值是false

參考型別和值

有三種reference 型別:類型別,陣列型別和介面型別。它們的值分別是對動態建立的類例項,陣列或類例項或實現介面的陣列的引用。

陣列型別由具有單個維度的 元件型別(其長度不是由型別給出)組成。陣列型別的元件型別本身可以是陣列型別。如果從任何陣列型別開始,考慮其元件型別,然後(如果它也是陣列型別)該型別的元件型別,依此類推,最終必須達到不是陣列型別的元件型別; 這稱為陣列型別的元素型別。陣列型別的元素型別必須是基本型別,類型別或介面型別。

reference值也可以是專用空引用的,沒有物件的引用,這將在這裡通過來表示null。該null引用最初沒有執行時型別,但可以轉換為任何型別。reference型別的預設值是null

該規範不要求具體的值編碼null

2.3 解析

解析階段是虛擬機器將常量池中的符號引用替換為直接引用的過程。 符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。 直接引用(Direct References):直接引用可以是直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制程式碼。如果有了直接引用,那麼引用的目標一定是已經存在於記憶體中。

解析物件包括:

2.3.1 類或者介面的解析

假設當前程式碼所處的類為D,如果要把一個從未解析過的符號引用N解析為一個類或介面C的引用,那虛擬機器完成整個解析過程需要以下3個步驟: (1)如果C不是一個陣列型別,那虛擬機器將會把代表N的全限定名傳遞給D的類載入器去載入這個類C。 (2)如果C是一個陣列型別,並且陣列的元素型別為物件,那將會按照第1點的規則載入陣列元素型別。 (3)如果上面的步驟沒有出現任何異常,那麼C在虛擬機器中實際上已經成為了一個有效的類或介面了,但在解析完成之前還要進行符號引用驗證,確認D是否具有對C的訪問許可權。如果發現不具備訪問許可權,則丟擲java.lang.IllegalAccessError異常。

2.3.2 欄位解析

首先解析欄位表內class_index項中索引的CONSTANT_Class_info符號引用,也就是欄位所屬的類或介面的符號引用,如果解析完成,將這個欄位所屬的類或介面用C表示,虛擬機器規範要求按照如下步驟對C進行後續欄位的搜尋。
(1)如果C 本身就包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則返回這個欄位的直接引用,查詢結束。
(2)否則,如果C中實現了介面,將會按照繼承關係從下往上遞迴搜尋各個介面和它的父介面如果介面中包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則返回這個欄位的直接引用,查詢結束。
(3)否則,如果C 不是java.lang.Object的話,將會按照繼承關係從下往上遞迴搜尋其父類,如果在父類中包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則返回這個欄位的直接引用,查詢結束。
(4)否則,查詢失敗,丟擲java.lang.NoSuchFieldError異常。

2.3.3 類方法解析

首先解析類方法表內class_index項中索引的CONSTANT_Class_info符號引用,也就是方法所屬的類或介面的符號引用,如果解析完成,將這個類方法所屬的類或介面用C表示,虛擬機器規範要求按照如下步驟對C進行後續類方法的搜尋。
(1)類方法和介面方法符號引用的常量型別定義是分開的,如果在類方法表中發現class_index中索引的C 是個介面,那就直接丟擲java.lang.IncompatibleClassChangeError異常。
(2)如果通過了第一步,在類C中查詢是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查詢結束。
(3)否則,在類C的父類中遞迴查詢是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查詢結束。
(4)否則,在類C實現的介面列表以及他們的父介面中遞迴查詢是否有簡單名稱和描述符都與目標相匹配的方法,如果存在相匹配的方法,說明類C是一個抽象類這時查詢結束,丟擲java.lang.AbstractMethodError異常。
(5)否則,宣告方法查詢失敗,丟擲java.lang.NoSuchMethodError。

2.3.4 介面方法解析

首先解析介面方法表內class_index項中索引的CONSTANT_Class_info符號引用,也就是方法所屬的類或介面的符號引用,如果解析完成,將這個介面方法所屬的介面用C表示,虛擬機器規範要求按照如下步驟對C進行後續介面方法的搜尋。
(1)與類解析方法不同,如果在介面方法表中發現class_index中的索引C是個類而不是個介面,那就直接丟擲java.lang.IncompatibleClassChangeError異常。
(2)否則,在介面C中查詢是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查詢結束。
(3)否則,在介面C的父介面中遞迴查詢,直到java.lang.Object類(查詢範圍包括Object類)為止,看是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查詢結束。
(4)否則,宣告方法查詢失敗,丟擲java.lang.NoSuchMethodError。

3. 初始化

  • 遇到newgetstaticputstaticinvokestatic這四個位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這四條命令的最常見的靜態欄位(被final修飾、已在編譯器把結果放入常量池的靜態欄位除外)的時候,以及呼叫一個類的靜態方法的時候
  • 使用反射對類進行呼叫,如果該類沒有進行初始化 ,則需要先觸發其初始化
  • 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  • 當虛擬機器啟動時,使用者需要制定一個需要執行的主類,即:包含main方法的類。虛擬機器會先初始化這個類。
不能被初始化的例子
  • 通過子類引用父類的靜態欄位,子類不會被初始化
  • 通過陣列定義來引用類
  • 呼叫類的常量(常量在編譯階段就存入呼叫類的常量池中了)
示例1:
public class Fantj {
    public static int high = 180;
    static {
        System.out.println("靜態初始化類Fantj ");
        high = 185;
    }
    public Fantj(){
        System.out.println("建立Fantj 類的物件");
    }
}
複製程式碼
public class Main {
    public static void main(String[] args) {
        Fantj fantj = new Fantj();
        System.out.println(fantj.high);
    }
}
複製程式碼

控制檯列印:

靜態初始化類Fantj 
建立Fantj 類的物件
185
複製程式碼
  1. jvm載入Main類,首先在方法區生成Main類對應的靜態變數靜態方法常量池程式碼等,同時在堆裡生成Class物件(反射物件),通過該物件可以訪問方法區資訊,類Fantj也是如此。
  2. main方法執行,一個方法對應一個棧幀,所以Fantj壓棧,一開始fantj 是空,Fantj壓棧的同時堆中生成Fantj物件,然後把物件地址交付給fantj,此時fantj 就擁有了Fantj物件地址。
  3. fantj.high 來呼叫方法區的資料。

好了,試試靜態方法。給Fantj類加個方法:

    public static void boss(){
        System.out.println("boss靜態方法初始化");
    }
複製程式碼
public class Main {
    public static void main(String[] args) {
//        Fantj fantj = new Fantj();
//        System.out.println(fantj.high);
        Fantj.boss();
    }
}
複製程式碼
靜態初始化類Fantj 
boss靜態方法初始化
複製程式碼

說明了呼叫靜態方法沒有對類進行例項化,所以靜態類載入會被初始化。

4. 類載入器

JVM的類載入是通過ClassLoader及其子類來完成的,虛擬機器設計團隊把載入動作放到JVM外部實現,以便讓應用程式決定如何獲取所需的類,JVM提供了3種類載入器:

4.1 啟動類載入器(Bootstrap ClassLoader):

負責載入 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath引數指定路徑中的,且被虛擬機器認可(按檔名識別,如rt.jar)的類。

4.2 擴充套件類載入器(Extension ClassLoader):

負責載入 JAVA_HOME\lib\ext目錄中的,或通過java.ext.dirs系統變數指定路徑中的類庫。

4.3 應用程式類載入器(Application ClassLoader):

負責載入使用者路徑(classpath)上的類庫。

4.4 自定義類載入器
  • 高度靈活
  • 實現熱部署
  • 程式碼加密

JVM通過雙親委派模型進行類的載入,當然我們也可以通過繼承java.lang.ClassLoader實現自定義的類載入器。

一個小Demo
/**
 * 只載入當前包下的類,不是當前包下的交給上面的類載入器
 * Created by Fant.J.
 */
public class MyClassLoader {
        ClassLoader classLoader = new ClassLoader() {

            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                //拿出類的簡單名稱
                String filename = name.substring(name.lastIndexOf(".") + 1) + ".class";

                InputStream ins = getClass().getResourceAsStream(filename);

                if (ins == null){
                    return super.loadClass(name);
                }
                try {
                    byte [] buff = new byte[ins.available()];
                    ins.read(buff);
                    // 將位元組碼轉換成類物件
                    return defineClass(name,buff,0,buff.length);
                } catch (Exception e) {
                    //
                    throw new ClassNotFoundException();
                }
            }
        };
}
複製程式碼

在自定義的載入器中,將本包以外的類的載入工作都交給父類載入器:return super.loadClass(name);

public abstract class Test {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MyClassLoader myClassLoader = new MyClassLoader();
        Object o = myClassLoader.classLoader.loadClass("com.jvm.classload.Son").newInstance();
        System.out.println("Son類物件:"+o.getClass());
        System.out.println("Son父類物件:"+o.getClass().getSuperclass());

        Object c = myClassLoader.classLoader.loadClass("com.jvm.classload.MyClassLoader").newInstance();
        System.out.println("c物件的類載入器:"+c.getClass().getClassLoader());
        System.out.println("myClassLoader物件的類載入器:"+myClassLoader.getClass().getClassLoader());
    }
}
複製程式碼

控制檯輸出:

Son類物件:class com.jvm.classload.Son
Son父類物件:class com.jvm.classload.Parent
c物件的類載入器:com.jvm.classload.MyClassLoader$1@4dc63996
myClassLoader物件的類載入器:sun.misc.Launcher$AppClassLoader@18b4aac2
複製程式碼

我們可以發現,同樣是MyClassLoader 這個物件,但是他們的類載入器不同,那他們就是不同的物件。

擴充

  1. 通過一個類的全限定名來獲取描述此類的二進位制位元組流
  2. 只有被同一個類載入器載入的類才可能會相等。相同的位元組碼被不同的類載入器載入的類不相等。
  3. 類的載入,會將其所有的父類都載入一遍,直到java.lang.Object。(因為Object是所有類的父類)

5. 雙親委派模型

通俗的講,就是某個特定的類載入器在接到載入類的請求時,首先將載入任務委託給父類載入器,依次遞迴,如果父類載入器可以完成類載入任務,就成功返回;只有父類載入器無法完成此載入任務時,才自己去載入。

JVM類載入機制

各個類載入器之間的層次關係被稱為類載入器的雙親委派模型。該模型要求除了頂層的啟動類載入器外,其餘的類載入器都應該有自己的父類載入器,而這種父子關係一般通過組合(Composition)關係來實現,而不是通過繼承(Inheritance)。

雙親委派模型是Java設計者推薦給開發者的類載入器的實現方式,並不是強制規定的。大多數的類載入器都遵循這個模型。

其實,該模型就是防止記憶體中出現多份同樣的位元組碼 。

參考文件:

  1. docs.oracle.com/javase/spec…
  2. www.importnew.com/25295.html
  3. blog.csdn.net/u011080472/…

相關文章