深入理解 JVM 類(lèi)加載機(jī)制

類(lèi)的生命周期一共分為七個(gè)階段:

JVM-類(lèi)加載機(jī)制

類(lèi)加載過(guò)程為加載、驗(yàn)證、準(zhǔn)備、解析和初始化五個(gè)部分,其中驗(yàn)證、準(zhǔn)備和解析三個(gè)部分又被稱(chēng)為 連接(Linking)

這些過(guò)程并不是嚴(yán)格的線性過(guò)程,中間會(huì)穿插執(zhí)行。比如加載為完成前,連接過(guò)程可能已經(jīng)開(kāi)始(比如文件格式的校驗(yàn));比如解析可能發(fā)生在初始化前也可能在初始化后等等。

現(xiàn)在對(duì)這 5 個(gè)階段做詳細(xì)分析。

1. 加載

把編譯后的 Class 文件載入內(nèi)存,創(chuàng)建一個(gè) java.lang.Class 對(duì)象。

這個(gè)階段要完成三件事情:

  • 使用類(lèi)的 全限定名 來(lái)獲取類(lèi)的二進(jìn)制字節(jié)流。
  • 將二進(jìn)制字節(jié)流的 靜態(tài)結(jié)構(gòu) 進(jìn)行轉(zhuǎn)化,存儲(chǔ)為方法區(qū)的 運(yùn)行時(shí)結(jié)構(gòu)。
  • 在內(nèi)存中生成 java.lang.Class 對(duì)象,作為訪問(wèn)的入口。經(jīng)常使用 Java 反射會(huì)對(duì) java.lang.Class 對(duì)象非常熟悉,因?yàn)榭梢詮脑搶?duì)象中獲取到很多類(lèi)的信息,比如字段、方法等。

Class 文件的來(lái)源可以多種多樣,由不同的類(lèi)加載器實(shí)現(xiàn)不同來(lái)源加載,比如:

  • 本地的 Class 文件直接加載。比如 classpath 路徑下的 .class 文件。
  • 網(wǎng)絡(luò)加載。
  • 壓縮包中加載,zip、jar 或者 war 加載。比如第三方類(lèi)庫(kù)打包壓縮在 jar 中,部署到 tomcat 的 Class 文件打包在 war 中。
  • 運(yùn)行時(shí)創(chuàng)建。Java 提供了一些工具類(lèi)來(lái)實(shí)現(xiàn),比如java.lang.reflect 包中的 Proxy 工具,可以直接在內(nèi)存中創(chuàng)建動(dòng)態(tài)代理對(duì)象。
  • 由其他文件生成,比如 JSP 文件。
JVM-類(lèi)加載機(jī)制-加載

我們可以也自定義類(lèi)加載器來(lái)實(shí)現(xiàn)從其他渠道加載 Class 文件。

載入內(nèi)存后,生成 java.lang.Class 對(duì)象非常特別,因?yàn)樗淮娣旁诜椒▍^(qū)中而不是堆中。

2. 驗(yàn)證

驗(yàn)證是連接的第一步。對(duì) Class 文件的格式檢查,確保滿(mǎn)足 JVM 規(guī)范,避免產(chǎn)生虛擬機(jī)的安全問(wèn)題。

因?yàn)樯厦嬲f(shuō)到 Class 文件不一定是 javac 編譯產(chǎn)生的,有各種方式可以創(chuàng)建,對(duì)于編譯時(shí)做的數(shù)組越界、對(duì)象類(lèi)型轉(zhuǎn)換錯(cuò)誤等問(wèn)題,類(lèi)加載過(guò)程還需要再次校驗(yàn)。

不過(guò)驗(yàn)證還是挺耗性能的,如果 Class 文件已經(jīng)被反復(fù)驗(yàn)證多次,可以使用 -Xverify:none 來(lái)縮短類(lèi)加載時(shí)間。

驗(yàn)證主要有四種驗(yàn)證:文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證和符號(hào)引用驗(yàn)證。

2.1. 文件格式驗(yàn)證

文件格式驗(yàn)證,確保符合 Class 文件規(guī)范,能被當(dāng)前版本的虛擬機(jī)處理。比如:

  • 檢查魔數(shù)(Magic Number)是否是 Class 文件(0xCAFEBABY)。

  • 檢查主次版本對(duì)不對(duì)。

  • 檢查常量池的類(lèi)型和索引是否正確。

    …...

在經(jīng)歷過(guò)這個(gè)階段后,字節(jié)流才會(huì)被存儲(chǔ)到方法區(qū)中。所以文件校驗(yàn)發(fā)生在 加載 階段還未結(jié)束前。經(jīng)過(guò)文件格式校驗(yàn)后,后續(xù)的校驗(yàn)都是基于方法區(qū)執(zhí)行的。

2.2. 元數(shù)據(jù)驗(yàn)證

元數(shù)據(jù)驗(yàn)證,字節(jié)碼語(yǔ)義分析,看是否符合語(yǔ)法規(guī)范。比如:

  • 是否有父類(lèi)。除了 java.lang.Object,所有的類(lèi)都要有父類(lèi)。

  • 是否繼承了不允許被繼承的類(lèi)。比如被 final 修飾類(lèi)不允許被繼承。

  • 如果不是抽象類(lèi),是否實(shí)現(xiàn)了父類(lèi)或接口要求實(shí)現(xiàn)的方法。

  • 是否類(lèi)的字段和方法和父類(lèi)有矛盾。比如覆蓋了父類(lèi)的 final 字段。

    …...

這些語(yǔ)法規(guī)范如果是編譯的 Class 文件,在編譯期間也會(huì)做校驗(yàn)。但因?yàn)?Class 文件來(lái)源多樣,并不能確保遵循語(yǔ)法規(guī)范,所以這里還要再進(jìn)行一次驗(yàn)證。

2.3. 字節(jié)碼驗(yàn)證

字節(jié)碼校驗(yàn)。對(duì)字節(jié)碼 數(shù)據(jù)流控制流 進(jìn)行分析,確定語(yǔ)義正確。這個(gè)是所有校驗(yàn)最復(fù)雜的階段。比如:

  • 正確的操作指令。操作操作局部變量表和操作數(shù)棧的數(shù)據(jù),指令類(lèi)型要正確。

  • 正確的跳轉(zhuǎn)指令。跳轉(zhuǎn)到正確的字節(jié)碼。

  • 正確的類(lèi)型轉(zhuǎn)換。比如子類(lèi)可以賦值給父類(lèi),但父類(lèi)不允許賦值子類(lèi)。

    …...

這里舉個(gè)校驗(yàn)的例子。

在執(zhí)行字節(jié)碼執(zhí)行的時(shí)候,對(duì)局部變量表和操作數(shù)棧的數(shù)據(jù)進(jìn)行操作時(shí),需要使用正確的指令類(lèi)型。比如局部變量表索引 1 是 int 類(lèi)型,加載它需要用 iload_1 字節(jié)碼,而不是 lload_1 、fload_1dload_1 。在元數(shù)據(jù)校驗(yàn)階段就可以做強(qiáng)制約束,排除錯(cuò)誤使用。

  public int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1                     # 因?yàn)榫植孔兞款?lèi)型為 int,使用 iload 加載                     
         1: iload_2
         2: iadd
         3: ireturn
      LineNumberTable:
        line 32: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  this   Loblee/demo/jvm/stack/SimpleObject;
            0       4     1     a   I   # 局部變量類(lèi)型為 int
            0       4     2     b   I   # 局部變量類(lèi)型為 int

經(jīng)過(guò)了復(fù)雜的字節(jié)碼驗(yàn)證,其實(shí)還是無(wú)法確保運(yùn)行的安全。屬于著名的 "Halting Problem"

復(fù)雜的驗(yàn)證也十分消耗性能,HotSpot 虛擬機(jī)做了很多優(yōu)化。

2.4. 符號(hào)引用驗(yàn)證

符號(hào)引用校驗(yàn)。這個(gè)發(fā)生在符號(hào)引用轉(zhuǎn)換為直接引用的時(shí)候,屬于 解析 階段的驗(yàn)證,用來(lái)確認(rèn)引用一定會(huì)被訪問(wèn)到。比如:

  • 符號(hào)引用的全限定名是否能找到對(duì)應(yīng)的類(lèi)。

  • 符號(hào)引用的字段描述符或方法描述符是否能在類(lèi)中找到對(duì)應(yīng)字段和方法。

  • 符號(hào)引用的類(lèi)、字段和方法的作用域(public、protected、private、default)是否可以被當(dāng)前類(lèi)訪問(wèn)。

    …...

符號(hào)引用驗(yàn)證也說(shuō)明了這幾個(gè)階段不是線性的,而是會(huì)會(huì)穿插執(zhí)行。

無(wú)法通過(guò)符號(hào)引用驗(yàn)證的話,會(huì)拋出相關(guān)的異常,比如 java.lang.IllegalAccessError 、 java.lang.NoSuchFieldError 、java.lang.NoSuchMethodError 等等。

3. 準(zhǔn)備

類(lèi)變量 分配內(nèi)存,設(shè)置初始值。

分配的初始值具體是多少?各個(gè)數(shù)據(jù)的默認(rèn)零值,有 0、0L、null、false 等。

比如下面的類(lèi)變量 a:

public class SimpleObject {
    public static String a = "hello world";
    public static String b = 2;
}

準(zhǔn)備 階段給 a 的初始值是 null,而不是 “hello world” 字符串,給 b 的初始值是 0 而不是 2。a 和 b 的真實(shí)賦值發(fā)生在 初始化 階段,放在了類(lèi)構(gòu)造器 <clinit>() 中,使用 putstatic 指令實(shí)現(xiàn)。

上面是類(lèi)變量的處理過(guò)程,常量并非如此。如果是字符串或者基礎(chǔ)數(shù)據(jù)類(lèi)型(int、long、float 等等)的常量類(lèi)型,準(zhǔn)備階段會(huì)直接分配具體數(shù)據(jù)。

還是 SimpleObject 對(duì)象,我們使用 final 關(guān)鍵字將 a 和 b 修飾為類(lèi)常量:

public class SimpleObject {
    public static final String a = "hello world";
    public static final String b = 2;
}

準(zhǔn)備階段,會(huì)直接從運(yùn)行時(shí)常量池中取出 "hello world" 字符串給 a 賦值,從常量池中取出 2 對(duì) b 賦值(這時(shí)候 2 被加入到常量池中)。

注意,準(zhǔn)備階段只是處理 類(lèi)變量,而不是 實(shí)例變量。實(shí)例變量的在類(lèi)實(shí)例化后一同和實(shí)例對(duì)象分配在堆中。

4. 解析

解析的過(guò)程,就是將符號(hào)引用轉(zhuǎn)為直接引用的過(guò)程。

符號(hào)引用中主要有:

  • 類(lèi)和接口的全限定名。
  • 字段名稱(chēng)和描述符。
  • 方法名稱(chēng)和描述符。

對(duì)應(yīng)在 class 文件中的常量類(lèi)型為 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 。編譯后在 class 文件的靜態(tài)常量池中,類(lèi)加載后進(jìn)入運(yùn)行時(shí)常量池。之所以需要符號(hào)引用,是因?yàn)?javac 將代碼編譯成 class 文件時(shí),是不知道這些類(lèi)、字段和方法在內(nèi)存中的位置的,需要有一個(gè)符號(hào)來(lái)代替。

比如我們有一個(gè)類(lèi) ObjectA ,引用了 ObjectB 作為成員變量:

public class  ObjectA {

    private ObjectB b;

    public void setB(ObjectB b) {
        this.b = b;
    }

    public ObjectB getB() {
        return b;
    }
}

使用 javap 查看這個(gè)類(lèi)的符號(hào)引用:

JVM-類(lèi)加載機(jī)制-符號(hào)引用

直接引用主要有:

  • 目標(biāo)指針,比如 Class 對(duì)象、類(lèi)變量、類(lèi)方法在方法區(qū)的指針。
  • 相對(duì)偏移量,比如實(shí)例變量在堆中的相對(duì)偏移量。
  • 能間接定位到目標(biāo)的 句柄。

可以直接引用對(duì)應(yīng)的目標(biāo)已經(jīng)在內(nèi)存中了。直接引用和虛擬機(jī)的內(nèi)存布局相關(guān),在不同虛擬機(jī)的直接引用是不一樣的。

這些在未解析前都是字符串,存在方法區(qū)的運(yùn)行時(shí)常量池里(HotSpot 1.8 方法區(qū)實(shí)現(xiàn)做了調(diào)整,符號(hào)引用被放到了由本地內(nèi)存組成的元空間中),字節(jié)碼解釋器在執(zhí)行字節(jié)碼的時(shí)候是無(wú)法直接使用符號(hào)引用的,所以需要翻譯成直接引用,可以是內(nèi)存中的

這個(gè)解析過(guò)程不一定在 初始化 前執(zhí)行,也有可能延遲到一個(gè)符號(hào)引用需要使用到的時(shí)候,在棧幀進(jìn)行動(dòng)態(tài)鏈接。

JVM-虛擬機(jī)棧-棧幀-動(dòng)態(tài)鏈接

初始化前的解析的被稱(chēng)為 靜態(tài)綁定 ,棧幀里的解析被稱(chēng)為 動(dòng)態(tài)綁定。

解析完成后,符號(hào)引用對(duì)應(yīng)的直接引用會(huì)被記錄在運(yùn)行時(shí)常量池中,后續(xù)如果重復(fù)解析,直接返回對(duì)應(yīng)的直接引用。

5. 初始化

經(jīng)過(guò)了加載和連接(校驗(yàn)、準(zhǔn)備和解析),類(lèi)已經(jīng)載入內(nèi)存并且分配了初始內(nèi)存,開(kāi)始進(jìn)行初始化。

加載過(guò)程我們可以自定義類(lèi)加載器(比如 ClassLoader) 來(lái)接管加載,連接完全是虛擬機(jī)自動(dòng)處理的,而初始化才開(kāi)始正式執(zhí)行字節(jié)碼。

初始化就是執(zhí)行類(lèi)構(gòu)造器 <clinit>() 的過(guò)程。<clinit>() 由類(lèi)變量賦值加上 static{} 語(yǔ)句塊的代碼合并而成,這個(gè)并不是必須的,如果沒(méi)有靜態(tài)變量賦值過(guò)程的話,是不會(huì)生成 <clinit>() 的。

<clinit()> 調(diào)用一些指令來(lái)實(shí)現(xiàn)靜態(tài)變量的賦值操作, 比如使用 putstatic 。

準(zhǔn)備類(lèi) SimpleObject 如下:

public class SimpleObject {
    public static String  a = "hello world";
    public static int     b = 100;
    public static ObjectC c = new ObjectC();
    public static Class   d = ObjectD.class;
    public static int     e = ObjectE.e;
    public static int     f = ObjectF.getF();
    public static int     h = 10;
    public static String  i = ObjectI.I;
    public static int     k = ObjectJ.k;

    static {
        ObjectH.h = h;
    }
}

使用 javap 查看 SimpleObject 的類(lèi)構(gòu)造器 <clinit>

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: ldc           #2                  // String hello world
         2: putstatic     #3                  // Field a:Ljava/lang/String;
         5: bipush        100
         7: putstatic     #4                  // Field b:I
        10: new           #5                  // class oblee/demo/jvm/pre/ObjectC
        13: dup
        14: invokespecial #6                  // Method oblee/demo/jvm/pre/ObjectC."<init>":()V
        17: putstatic     #7                  // Field c:Loblee/demo/jvm/pre/ObjectC;
        20: ldc           #8                  // class oblee/demo/jvm/pre/ObjectD
        22: putstatic     #9                  // Field d:Ljava/lang/Class;
        25: getstatic     #10                 // Field oblee/demo/jvm/pre/ObjectE.e:I
        28: putstatic     #11                 // Field e:I
        31: invokestatic  #12                 // Method oblee/demo/jvm/pre/ObjectF.getF:()I
        34: putstatic     #13                 // Field f:I
        37: bipush        10
        39: putstatic     #14                 // Field h:I
        42: ldc           #16                 // String objectI
        44: putstatic     #17                 // Field i:Ljava/lang/String;
        47: getstatic     #18                 // Field oblee/demo/jvm/pre/ObjectJ.k:I
        50: putstatic     #19                 // Field k:I
        53: getstatic     #14                 // Field h:I
        56: putstatic     #20                 // Field oblee/demo/jvm/pre/ObjectH.h:I
        59: return

如果靜態(tài)變量是 字符串類(lèi)型,作為字面量加入了常量池,初始化時(shí)從常量池中取出賦值。比如上面的 "hello world" 直接從常量池取出賦值給 a。

如果靜態(tài)變量是 基礎(chǔ)數(shù)據(jù)類(lèi)型,直接使用指令載入。比如上面的 bipush 指令。

如果靜態(tài)變量是個(gè) 類(lèi)實(shí)例的引用類(lèi)型,但是類(lèi)還未加載,會(huì)再次觸發(fā)這個(gè)類(lèi)的類(lèi)加載流程。

初始化不是在類(lèi)加載后就馬上執(zhí)行的,有一定的觸發(fā)條件。JVM 規(guī)范沒(méi)有規(guī)定什么時(shí)候開(kāi)始進(jìn)行類(lèi)加載流程,但對(duì)類(lèi)初始化有進(jìn)行嚴(yán)格的規(guī)定,有四條字節(jié)碼指令 new 、 getstatic 、putstatic 、invokestatic 這 4 條指令,如果沒(méi)有初始化會(huì)先觸發(fā)初始化。

常見(jiàn)的場(chǎng)景有:

  • 使用 new 關(guān)鍵詞實(shí)例化對(duì)象。上面對(duì)靜態(tài)變量 c 賦值,需要對(duì)類(lèi) ObjectC 進(jìn)行實(shí)例化,所以會(huì)觸發(fā) ObjectC 的初始化。
  • 讀取類(lèi)的靜態(tài)變量。上面賦值靜態(tài)變量 e, getstatic #10 ,觸發(fā) ObjectE 的初始化。
  • 設(shè)置類(lèi)的靜態(tài)變量。上面設(shè)置 ObjectH 的靜態(tài)變量 h,putstatic #20 ,觸發(fā) ObjectH 的初始化。
  • 調(diào)用類(lèi)的靜態(tài)方法。上面調(diào)用 ObjectF 的靜態(tài)方法 getF() ,invokestatic #12 ,觸發(fā) ObjectF 的初始化。

比如下面幾種情況不會(huì)執(zhí)行類(lèi)初始化:

  • 通過(guò)子類(lèi)引用父類(lèi)的靜態(tài)字段,只觸發(fā)父類(lèi)的初始化,不會(huì)觸發(fā)子類(lèi)的初始化。
  • 使用對(duì)象數(shù)組,不會(huì)觸發(fā)該類(lèi)初始化。
  • 常量被存入常量池中,沒(méi)有直接引用常量的類(lèi),不會(huì)觸發(fā)定義常量的類(lèi)的初始化。
  • 通過(guò)類(lèi)名獲取 Class 對(duì)象,不會(huì)觸發(fā)初始化。比如上面使用 ObjectD.class 并不會(huì)觸發(fā) ObjectD 類(lèi)的初始化。
  • 使用 Class.forName() 加載類(lèi),參數(shù) initialize 為 false 不會(huì)觸發(fā)初始化。
  • 通過(guò) ClassLoader 默認(rèn)的 loadClass 方法加載類(lèi)不會(huì)觸發(fā)初始化。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 編譯原理請(qǐng)查看之前內(nèi)容,.java文件編譯過(guò)程和執(zhí)行過(guò)程分析以及計(jì)算機(jī)簡(jiǎn)單認(rèn)識(shí) 需要了解更多Java原創(chuàng)資料,請(qǐng)加...
    坑王老薛閱讀 2,386評(píng)論 0 5
  • 概述 虛擬機(jī)把描述類(lèi)的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn),轉(zhuǎn)換,解析和初始化,最終形成可以被虛擬機(jī)直...
    lwd45閱讀 1,776評(píng)論 0 16
  • 簡(jiǎn)述:虛擬機(jī)把描述類(lèi)的數(shù)據(jù)從class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接...
    卡巴拉的樹(shù)閱讀 1,935評(píng)論 1 6
  • 前言 什么是類(lèi)加載? 虛擬機(jī)把描述類(lèi)的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成...
    JackieLeeQJ閱讀 527評(píng)論 0 0
  • 文/雨隨塵清 曾經(jīng)的你 如一片秋葉 獨(dú)自凋零在夏天的尾聲 人們常說(shuō)一葉知秋 可誰(shuí)記得荒野中的你 誰(shuí)懂被世界遺忘的落...
    清陋閱讀 1,064評(píng)論 17 21

友情鏈接更多精彩內(nèi)容