Java虛擬機類加載機制

Java類加載的過程

類從被加載到虛擬機內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期包括:加載、驗證、準備、解析、初始化、使用和卸載七個階段。

對于第一階段的加載,Java虛擬機規(guī)范中沒有強制約束。但是初始化只有五種情況:

  1. 用new關鍵字是花花對象,調(diào)用和設置類的靜態(tài)字段,調(diào)用一個類的靜態(tài)方法。
  2. 使用反射對類進行調(diào)用,如果類沒有初始化會先將其初始化。
  3. 但初始化一個類時,發(fā)現(xiàn)其父類沒有初始化,會先初始化其父類
  4. 但虛擬機啟動時,用戶需要指定一個要執(zhí)行的主類。
  5. 當使用jdk1.7的胴體語言支持時。

這五種場景中的行為稱為主動引用,所有被動引用都不會觸發(fā)初始化。

  1. 通過子類引用父類的靜態(tài)字段,不會導致子類初始化。
  2. 通過數(shù)組定義類引用類,不會觸發(fā)此類的初始化
  3. 常量不會觸發(fā)類的初始化(final修飾的變量在編譯階段就被存入調(diào)用類的常量池中)。

接口的加載和過程和類的加載過程有一些不同,接口只有在真正使用父接口的時候才會初始化父接口。

加載階段

  1. 通過類的全限定名來獲取定義此類的二進制字節(jié)流。
  2. 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。
  3. 在內(nèi)存中生成一個代表這個類的java.lang.class的對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。

數(shù)組類本身不通過類加載器創(chuàng)建,它是由Java虛擬機直接創(chuàng)建的。數(shù)組類的元素類型最終是由類加載器創(chuàng)建的。

  1. 如果數(shù)組的組件類型是引用類型,就使用上述的加載過程去創(chuàng)建這個類型
  2. 如果不是引用類型虛擬機就會把數(shù)組標記為與引導類加載器管理
  3. 如果組件類型不是引用類型,則數(shù)組的默認可見性為public。

加載完成后,虛擬機外部的二進制字節(jié)流就按照虛擬機所需的格式存儲在方法區(qū)之中。Class對象雖然是一個對象,但是存放在方法區(qū)里。

驗證階段

驗證是連接階段的第一步,主要保證Class文件的字節(jié)流中包含的信息符合虛擬機的要求。驗證階段有四個階段的校驗動作:

  1. 文件格式校驗:
    • 是否以魔數(shù)開頭
    • 主次版本號是否在虛擬機處理范圍內(nèi)
    • 常量池中是否有不被支持的常量類型
    • 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量
    • CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的數(shù)據(jù)
    • Class文件中各個部分及文件本身是否有被刪除的或附加的其他信息
  2. 元數(shù)據(jù)校驗:
    • 驗證當前類是否有父類
    • 是否繼承了final類
    • 如果不是抽象類是否實現(xiàn)了所有方法
    • 類中的字段、方法是否與父類產(chǎn)生矛盾
  3. 字節(jié)碼校驗:
    • 保證任意時刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼都能配合工作
    • 保證跳轉(zhuǎn)指令不會跳轉(zhuǎn)到防反彈以外的字節(jié)碼指令上
    • 保證方法體重的類型轉(zhuǎn)換是有效的
  4. 符合引用校驗:
    • 符合引用中通過字符串描述的全限定名是否能找到對應的類
    • 在指定類中是否存在符合方法的字段描述符以及簡單名稱鎖描述的方法和字段
    • 符合引用中的類、字段、方法的訪問性是否可被當前類訪問

準備階段

準備階段是正式為類變量分配內(nèi)存并設置初始化值的階段。這些變量所使用的內(nèi)存都在方法區(qū)中分配。這時候分配的內(nèi)存僅包括類變量(static修飾),實例變量將在對象實例化的時候一起分配到堆中。通常情況下初始值為數(shù)據(jù)類型的零值,賦值操作在初始化階段才會執(zhí)行。

public static int value = 1; //初始值為0
public static boolean value = true; //初始值為false
public static char value = 'a'; //初始值為'\u0000'

如果字段屬性存在常量屬性則準備階段直接賦值。

public static final int value = 1; //直接賦值為1

解析階段

解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程。

  • 符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現(xiàn)的內(nèi)存布局無關,引用的目標并不一定加載到內(nèi)存中。
  • 直接引用:直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是和虛擬機實現(xiàn)的內(nèi)存布局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不相同。

解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點限定符7類符合引用。

初始化階段

類初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程。

  • <clinit>()方法是編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊(statis{})中的語句合并產(chǎn)生的。
  • <clinit>()方法與類的構(gòu)造函數(shù)不同,它不需要顯示的調(diào)用父類構(gòu)造器,虛擬機會保證在子類的<client>()方法執(zhí)行之前,父類的<client>()方法已經(jīng)執(zhí)行完畢。
  • 由于父類的<clinit>()方法先執(zhí)行,父類的靜態(tài)語句塊要優(yōu)先于子類的變量賦值操作。
  • <clinit>()方法對于類和接口來說并不是必須的,如果類沒有靜態(tài)語句塊,也沒有變量的賦值操作,那么編譯器就不會生成<clinit>()方法。
  • 如果接口有變量初始化賦值操作,接口與類一樣都會生成<clinit>()方法。接口不需要先執(zhí)行父類的<clinit>()方法,接口的實現(xiàn)類也不需要先執(zhí)行接口的<clinit>()方法。
  • 在多線程環(huán)境中,虛擬機也會保證一個類的<clinit>()方法被正確的加鎖、同步。

類加載過器

類加載器是Java運行環(huán)境的一部分,負責動態(tài)加載Java類到Java虛擬機的內(nèi)存空間。類通常是按需加載的,即第一次使用該類時才加載。JVM有三個默認的類加載器

  • 引導(BootStrap)類加載器,負責將放在<JAVA_HOME>\lib目錄中或者被-Xbootclasspath參數(shù)所指定的路徑中的,并且是虛擬機識別的類庫加載到內(nèi)存中。
  • 擴展(Extensions)類加載器,由sun.misc.Launcher$ExtClassLoader實現(xiàn),負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫。
  • 系統(tǒng)(Application)類加載器,負責加載用戶類路徑上所指定類庫。可以直接使用(ClassLoader.getSystemClassLoader()獲取)

類加載器直接的關系是雙親委派模型。要求除了頂層加載器外,其余的類加載器都應當有直接的父類加載器。類的父子關系是以組合關系類實現(xiàn)的。

獲取類加載器:

    //系統(tǒng)類加載器
    ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
    System.out.println(appClassLoader);
    //擴展類加載器
    ClassLoader extClassLoader = appClassLoader.getParent();
    System.out.println(extClassLoader);
    //啟動類加載器
    ClassLoader bsClassLoader = extClassLoader.getParent();
    System.out.println(bsClassLoader);

輸出:

sun.misc.Launcher$AppClassLoader@2626b418
sun.misc.Launcher$ExtClassLoader@4617c264
null

系統(tǒng)類加載器可以直接獲取,擴展類加載器的也可以使用,不過啟動類加載器是由C++實現(xiàn)的,邏輯上是不存在的,所以為null。

其他

Java魔數(shù):SUN公司規(guī)定每個Class文件都必須以一個word(4個字節(jié))開始,它的唯一作用是確定這個文件是否為一個能被虛擬機接收的Class文件。用十六進制打開Class文件就可以看到Class文件的魔數(shù)是0XCAFEBABE。

主次版本號:魔數(shù)的后續(xù)的內(nèi)容就是一個word的長度來表示生產(chǎn)的class文件的版本號,版本號分為主版本號和次版本號,高版本JDK可以向下兼容以前的Class文件,但不能運行以后版本的Class文件。

編譯器版本 十六進制版本號 十進制版本號
JDK1.9 0X35 53
JDK1.8 0X34 52
JDK1.7 0X33 51
JDK1.6 0X32 50
JDK1.5 0X31 49
JDK1.4 0X30 48
JDK1.3 0X2F 47
JDK1.2 0X2E 46
JDK1.1 0X2D 45

常量池的tag項說明

常量類型
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_INterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

參考鏈接

  1. 深入理解Java虛擬機(6)-Class類文件結(jié)構(gòu)
  2. Java類加載器
  3. Java自定義類加載器與雙親委派模型
  4. Mac 十六進制編譯器
  5. 《深入理解Java虛擬機》
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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

  • 讀書筆記 深入理解Java虛擬機:JVM高級特性與最佳實現(xiàn)(第二版) 概述 深入了解了Class文件存儲格式的具...
    Bollen_Chak閱讀 568評論 0 1
  • 什么叫類加載 JVM 將Class文件加載到內(nèi)存,并對數(shù)據(jù)進行校驗、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機直接使用...
    那些年未曾努力過閱讀 475評論 0 0
  • Java程序運行于Java虛擬機之上,JVM屏蔽了底層細節(jié),使得Java程序能夠“一次編譯,到處運行”。在Java...
    程序之心閱讀 380評論 0 8
  • 虛擬機把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進行校驗、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機直接使用的...
    云飛揚1閱讀 1,685評論 2 51
  • 什么是虛擬機的類加載機制?虛擬機把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進行校驗、轉(zhuǎn)換解析以及初始化,最...
    EakonZhao閱讀 2,438評論 6 22

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