《深入理解JVM》---- 類加載機(jī)制

概述

虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn),轉(zhuǎn)換,解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型,這就是Java的類加載機(jī)制。
在Java語言中,類的加載,連接和初始化過程都是在程序運(yùn)行期間完成的也就是動(dòng)態(tài)性,雖然會(huì)增加類的加載性能開銷,但是這也為java應(yīng)用程序提供高度的靈活性

類加載的時(shí)機(jī)

類被加載到虛擬機(jī)內(nèi)存開始,到卸載出內(nèi)存為止,生命周期包含: ** 加載,驗(yàn)證,準(zhǔn)備,解析,初始化,使用,卸載 ** 7個(gè)階段,加載,驗(yàn)證,準(zhǔn)備,初始化和卸載這5個(gè)順序是確定的,解析階段則不一定,他在某些情況下可以在初始化階段之后在開始,這是為了支持Java語言的運(yùn)行時(shí)綁定。

類加載機(jī)制.png
初始化

Java虛擬機(jī)嚴(yán)格規(guī)定 ** 有且僅有 ** 五種情況必須立即對(duì)類進(jìn)行初始化

  • 遇到new,getstatic, putstatic, 或者 invokestatic 這4條字節(jié)碼指令時(shí),如果沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。常見場景有使用new實(shí)例化對(duì)象,讀取或設(shè)置一個(gè)類的靜態(tài)字段,調(diào)用一個(gè)類的靜態(tài)方法。
  • 使用java.lang.reflect包的方法隊(duì)里進(jìn)行反射調(diào)用的時(shí)候,如果沒有進(jìn)行過初始化,則需先觸發(fā)其初始化。
  • 當(dāng)初始化一個(gè)類的時(shí)候,發(fā)現(xiàn)其父類沒有進(jìn)行初始化,則先觸發(fā)其父類的初始化。
  • 當(dāng)虛擬機(jī)啟動(dòng)的時(shí)候,用戶需要指定一個(gè)需要執(zhí)行的主類(包含main()方法的那個(gè)類),虛擬機(jī)會(huì)先初始化這個(gè)主類
  • 當(dāng)使用JDK 1.7的動(dòng)態(tài)語言支持時(shí),如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后解析結(jié)果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且這個(gè)方法句柄所對(duì)應(yīng)的類沒有進(jìn)行過初始化,則需先要觸發(fā)其初始化。
    ** 如下代碼: 通過子類應(yīng)用父類的靜態(tài)字段,不會(huì)導(dǎo)致子類被初始化**
public class SuperClass {
    /**
     * 被動(dòng)使用類字段
     * 通過子類應(yīng)用父類的靜態(tài)字段,不會(huì)導(dǎo)致子類被初始化
     */
    static{
        System.out.println("SuperClass init!");
    }
    
    public static int value = 100;
}

public class SubClass extends SuperClass{
    static {
        System.out.println("SubClass init!");
    }
}

/**
 * 非主動(dòng)使用類字段演示
 */
public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLO);
    }
}

上面代碼運(yùn)行后只會(huì)輸出“SuperClass init! ”,對(duì)于靜態(tài)代碼字段,只有直接定義這個(gè)字段的類才會(huì)被初始化,因此通過子類引用父類中靜態(tài)字段,只會(huì)觸發(fā)父類的初始化而不會(huì)觸發(fā)之類的初始化。

** 通過數(shù)組定義來引用該類,不會(huì)導(dǎo)致此類的初始化 **

public class NotInitialization {
    public static void main(String[] args) {
        SuperClass[] sca = new SuperClass[10];
    }
}

以上代碼不會(huì)有輸出

** 常量在編譯階段會(huì)存入調(diào)用類的常量池中本質(zhì)上沒有直接引用到定義常量的類,因此不會(huì)觸發(fā)定義常量類的初始化 **

public class ConstClass {
    
    static {
        System.out.println("ConstantClass init!");
    }
    
    public static final String HELLO = "hello world";
}

public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLO);
    }
}

以上只會(huì)輸出 hello world

類的加載過程

也就是 加載,驗(yàn)證,準(zhǔn)備, 解析, 初始化 這五個(gè)階段

1)加載

在加載階段,虛擬機(jī)主要完成以下3件事

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

加載階段完成后,虛擬機(jī)外部的二進(jìn)制字節(jié)流就按照虛擬機(jī)所需要的格式存儲(chǔ)在方法區(qū)之中,方法區(qū)中的數(shù)據(jù)存儲(chǔ)格式由虛擬機(jī)實(shí)現(xiàn)自行定義,虛擬機(jī)規(guī)范未規(guī)定此區(qū)域的具體數(shù)據(jù)結(jié)構(gòu)。然后在內(nèi)存中實(shí)例化一個(gè)java.lang.Class類的對(duì)象,這個(gè)對(duì)象將作為程序訪問方法區(qū)中的這些類型數(shù)據(jù)的外部接口。

2)驗(yàn)證

驗(yàn)證時(shí)連接階段的第一步,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全,主要包含以下內(nèi)容

  • 文件格式驗(yàn)證:
    第一階段要驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理包含以下內(nèi)容
  1. 是否以魔數(shù)0xCAFEBABE開頭
  2. 主次版本號(hào)是否在當(dāng)前虛擬機(jī)處理范圍之內(nèi)
  3. 常量池的常量中是否有不被支持的常量類型
  4. 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量
  5. CONSTANT_Utf8_info 型的常量中是否有不符合UFT8編碼的數(shù)據(jù)
  6. Class文件中各個(gè)部分及文件本身是否有本刪除的或附加的其他信息
    ·····
    只有通過了這個(gè)階段的驗(yàn)證后,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)中進(jìn)行存儲(chǔ),所以后面的3個(gè)驗(yàn)證階段全部是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)進(jìn)行的,不會(huì)再直接操作字節(jié)流。
  • 元數(shù)據(jù)驗(yàn)證
    第二階段是對(duì)字節(jié)碼描述的信息進(jìn)行語義分析,以保證其描述的信息符合Java語言的規(guī)范的要求,包含以下信息
  1. 這個(gè)類是否有父類(除了java.lang.Object之外,所有的類都應(yīng)當(dāng)有父類)
  2. 這個(gè)類的父類是否繼承了不允許被繼承的類
  3. 如果這個(gè)類不是抽象類,是否實(shí)現(xiàn)了其父類或者接口要求實(shí)現(xiàn)的所有的方法
  4. 類中的字段,方法是否與父類產(chǎn)生矛盾
    ······
    第二階段主要目的是對(duì)類的元素?fù)?jù)信息進(jìn)行語義校驗(yàn),保證不存在不符合Java語言規(guī)范的元數(shù)據(jù)信息
  • 字節(jié)碼驗(yàn)證
    第三階段是整個(gè)驗(yàn)證過程中最復(fù)雜的一個(gè)階段,主要目的是通過數(shù)據(jù)流和控制流分析,確定程序語法是否是合法的,符合邏輯的。在第二階段對(duì)元數(shù)據(jù)信息中的數(shù)據(jù)類型做完校驗(yàn)后,這個(gè)階段將對(duì)類的方法體進(jìn)行校驗(yàn)分析,保證被校驗(yàn)類的方法在運(yùn)行時(shí)候不會(huì)做出對(duì)虛擬機(jī)有危害的事情。包含如下內(nèi)容
  1. 保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作,例如不會(huì)出現(xiàn)類似這種情況:在操作棧中放置一個(gè)int類型數(shù)據(jù),使用卻將他按照long類型使用
  2. 保證跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上
  3. 保證方法體中的類型轉(zhuǎn)換是有效的,例如可以把一個(gè)子類對(duì)象賦給父類數(shù)據(jù)類型,這是安全的,但是把父類對(duì)象賦值給子類數(shù)據(jù)類型則是危險(xiǎn)的
    ······
  • 符號(hào)引用驗(yàn)證
    最后一個(gè)階段的校驗(yàn)發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候,這個(gè)轉(zhuǎn)換動(dòng)作發(fā)生在連接的第三階段--解析中發(fā)生,符號(hào)引用可以看做是對(duì)類自身以外的信息進(jìn)行匹配性校驗(yàn),通常需要校驗(yàn)一下內(nèi)容:
  1. 符號(hào)引用中通過字符串描述的全限定名是否能找到對(duì)應(yīng)的類
  2. 在指定的類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段
  3. 符號(hào)引用中的類,字段,方法的訪問性是否可以被當(dāng)前類訪問。
3)準(zhǔn)備

準(zhǔn)備階段是正式為** 類變量 **分配內(nèi)存并設(shè)置變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配,記住,只為類變量分配內(nèi)存,不包括實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨對(duì)象一起分配在java堆中

數(shù)據(jù)初值

如果類字段的字段屬性表中存在ConstantValue屬性,那么在準(zhǔn)備階段變量value就會(huì)被初始化為ConstantValue屬性所指定的值,例如:

public static final int value = 123;//在準(zhǔn)備階段虛擬機(jī)就會(huì)根據(jù)ConstantValue設(shè)置將value賦值為123
4)解析

解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過程,對(duì)同一個(gè)符號(hào)引用進(jìn)行多次解析請(qǐng)求是很常見的事情,虛擬機(jī)不會(huì)重新再解析而是通過緩存去拿出解析的數(shù)據(jù),但是invokedynamic指令除外,它會(huì)每次被解析都會(huì)被重新解析,解析動(dòng)作主要針對(duì)類,接口,字段,類方法,接口方法,方法類型,方法句柄和調(diào)用點(diǎn)限定符7類符號(hào)引用進(jìn)行,主要包含以下內(nèi)容

符號(hào)引用:符號(hào)引用以一組符號(hào)來描述所引用的目標(biāo),符號(hào)可以是任意形式的字面量,只要使用時(shí)能夠無歧義的定位到目標(biāo)即可。
直接引用:直接引用可以是直接指向目標(biāo)的指針,相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。

  • 類或接口的解析
  • 字段解析
  • 類方法解析
  • 接口方法解析
5)初始化

類初始化階段是類加載過程的最后一步,前面的類加載過程中,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器參與之外,其余動(dòng)作全部由虛擬機(jī)主導(dǎo)和控制,到了初始化階段,才真正開始執(zhí)行類中定義的Java程序代碼,在準(zhǔn)備階段變量已經(jīng)賦過一次系統(tǒng)要求的初始值,而在初始化階段則通過程序制定的主觀計(jì)劃去初始化變量和其他資源,從另一個(gè)角度理解就是執(zhí)行類構(gòu)造器<clinit>()方法的過程

  • <clinit>()方法是由編譯器自動(dòng)收集類中的所有變量的賦值動(dòng)作和靜態(tài)語句塊中的語句合并產(chǎn)生的,他按照代碼中出現(xiàn)的順序收集,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量,定義在他之后的,在靜態(tài)語句塊中只能賦值不能訪問
public class Test {
    static{
        i = 1;//可以賦值
        System.out.println(i);//不能訪問
    }
    static int i = 0;
}
  • <clinit>()方法在執(zhí)行之前必須保證自己父類的類構(gòu)造器方法已經(jīng)執(zhí)行完畢,因此在虛擬機(jī)中第一個(gè)被執(zhí)行的<clinit>()方法的類肯定是java.lang.Object
  • 由于父類的<clinit>()方法優(yōu)先執(zhí)行,意味著父類中定義的靜態(tài)語句塊要優(yōu)先于子類的變量賦值操作
  • <clinit>()并不是必須的,如果一個(gè)類中沒有靜態(tài)語句塊,也沒有對(duì)變量的賦值操作,那么編譯器可以不為這個(gè)類生成<clinit>()方法。
  • 接口中不能使用靜態(tài)語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會(huì)生成<clinit>()方法,但是接口與類不同的是,執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父類接口的<clinit>()方法,只有父類接口中定義的變量使用時(shí)父類接口才會(huì)初始化,另外接口實(shí)現(xiàn)類在初始化時(shí)也一樣不會(huì)執(zhí)行接口的<clinit>()方法
  • 虛擬機(jī)會(huì)保證一個(gè)類的<clinit>()方法在多線程環(huán)境中被正確的加鎖,同步

類加載器

把類加載階段中“通過一個(gè)類的全限定名來獲取描述此類的二進(jìn)制字節(jié)流”這個(gè)動(dòng)作放到j(luò)ava虛擬機(jī)外部去實(shí)現(xiàn),以便讓程序自己決定如何去獲取所需要的類,實(shí)現(xiàn)這個(gè)動(dòng)作的代碼模塊稱為“類加載器”

1)類與類的加載器

比較兩個(gè)類是否相等,只有在這兩個(gè)類是由同一個(gè)類加載器加載的前提下才有意義,即使兩個(gè)類來源于同一個(gè)Class文件,被同一個(gè)虛擬機(jī)加載,只要他們的類加載器不一樣,那么這兩個(gè)類必定不相等(equals() isAssignableFrom() isInstance())

2)雙親委派模型

從java虛擬機(jī)的角度來講,只存在兩種不同的類加載器:一種是啟動(dòng)類加載器,是虛擬機(jī)的一部分,另一種是所有其他的類加載器,這些加載器由java語言實(shí)現(xiàn),獨(dú)立虛擬機(jī)之外,都繼承抽象類java.lang.ClassLoader

類加載器可以分為以下幾種
  • 啟動(dòng)類加載器(Bootstrap ClassLoader)
  • 擴(kuò)展類加載器(Extension ClassLoader)
  • 應(yīng)用程序類加載器(Application ClassLoader):一般情況下這個(gè)是程序默認(rèn)的類加載器

** 以下是類加載器的雙親委派模型**

類加載器雙親委派模型.png

類加載器之間父子關(guān)系一般不會(huì)以繼承的關(guān)系來實(shí)現(xiàn),都是使用組合關(guān)系來復(fù)用父加載器的代碼,如果一個(gè)類加載器收到類加載請(qǐng)求,他首先不會(huì)自己去嘗試加載這個(gè)類,而是將這個(gè)請(qǐng)求委派給父類加載器去完成,每一個(gè)層次都是如此,因此所有的加載請(qǐng)求最終都應(yīng)該傳送到頂層的啟動(dòng)類加載器,只有當(dāng)父類反饋無法完成這個(gè)加載請(qǐng)求時(shí),子加載器才會(huì)嘗試自己去加載。他的一個(gè)好處是Java類隨著他的類加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系,例如java.lang.Object,他存放在rt.jar中,無論哪一個(gè)類加載器要加載這個(gè)類,最終都是委派給啟動(dòng)類加載器進(jìn)行加載,因此Object類在程序的各種類加載環(huán)境中都是同一個(gè)類

最后編輯于
?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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