日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪(fǎng)問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 运维知识 > windows >内容正文

windows

JVM学习-类加载机制

發(fā)布時(shí)間:2024/1/16 windows 19 coder
生活随笔 收集整理的這篇文章主要介紹了 JVM学习-类加载机制 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

文章原文:https://gaoyubo.cn/blogs/4b481fd7.html

一、類(lèi)加載機(jī)制

在JVM學(xué)習(xí)-Class文件結(jié)構(gòu)中,講了Class文件存儲(chǔ)格式的具體細(xì)節(jié)。雖然Class文件中描述了各種類(lèi)信息,但要讓這些信息在虛擬機(jī)中運(yùn)行和使用,就需要加載到內(nèi)存中。本章將重點(diǎn)介紹虛擬機(jī)的類(lèi)加載機(jī)制,包括Class文件如何加載到內(nèi)存、加載后的信息發(fā)生何種變化等方面的內(nèi)容。

Java虛擬機(jī)通過(guò)將描述類(lèi)的數(shù)據(jù)從Class文件加載到內(nèi)存中,進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終生成可以被虛擬機(jī)直接使用的Java類(lèi)型。這一過(guò)程即為虛擬機(jī)的類(lèi)加載機(jī)制。與那些在編譯時(shí)需要進(jìn)行連接的語(yǔ)言不同,Java語(yǔ)言中類(lèi)型的加載、連接和初始化過(guò)程都在程序運(yùn)行期間完成。盡管這種策略可能導(dǎo)致編譯時(shí)的一些困難和類(lèi)加載時(shí)的性能開(kāi)銷(xiāo)略微增加,但它為Java應(yīng)用程序提供了極高的擴(kuò)展性和靈活性。Java天生支持動(dòng)態(tài)擴(kuò)展的語(yǔ)言特性依賴(lài)于運(yùn)行時(shí)的動(dòng)態(tài)加載和動(dòng)態(tài)連接

例如,編寫(xiě)一個(gè)面向接口的應(yīng)用程序,可以在運(yùn)行時(shí)指定其實(shí)際的實(shí)現(xiàn)類(lèi)。用戶(hù)可以通過(guò)Java預(yù)置的或自定義的類(lèi)加載器,在運(yùn)行時(shí)從網(wǎng)絡(luò)或其他位置加載一個(gè)二進(jìn)制流作為程序代碼的一部分。這種動(dòng)態(tài)組裝應(yīng)用的方式已廣泛應(yīng)用于Java程序,涵蓋了從基礎(chǔ)的Applet、JSP到相對(duì)復(fù)雜的OSGi技術(shù)。這一創(chuàng)新的方法使得Java語(yǔ)言能夠適應(yīng)多樣化的應(yīng)用需求。

二、類(lèi)加載時(shí)機(jī)

一個(gè)類(lèi)型從被加載到虛擬機(jī)內(nèi)存中開(kāi)始,到卸載出內(nèi)存為止,它的整個(gè)生命周期將會(huì)經(jīng)歷加載 (Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸載(Unloading)七個(gè)階段,其中驗(yàn)證、準(zhǔn)備、解析三個(gè)部分統(tǒng)稱(chēng)為連接(Linking)。這七個(gè)階段的發(fā)生順序如下:

類(lèi)加載過(guò)程包括加載、驗(yàn)證、準(zhǔn)備、初始化和卸載這五個(gè)階段。這些階段的順序是確定的,必須按部就班地開(kāi)始,而解析階段則不一定。解析階段在某些情況下可以在初始化階段之后再開(kāi)始,以支持Java語(yǔ)言的運(yùn)行時(shí)綁定特性(動(dòng)態(tài)綁定或晚期綁定)。值得注意的是,這些階段通常是互相交叉地混合進(jìn)行的,在一個(gè)階段執(zhí)行的過(guò)程中可能調(diào)用、激活另一個(gè)階段。

關(guān)于何時(shí)需要開(kāi)始類(lèi)加載過(guò)程的第一個(gè)階段“加載”,《Java虛擬機(jī)規(guī)范》中并沒(méi)有強(qiáng)制約束,這點(diǎn)可以由虛擬機(jī)的具體實(shí)現(xiàn)*把握。然而,在初始化階段,《Java虛擬機(jī)規(guī)范》明確規(guī)定了六種情況必須立即對(duì)類(lèi)進(jìn)行“初始化”(加載、驗(yàn)證、準(zhǔn)備自然需要在此之前開(kāi)始):

  1. 遇到newgetstaticputstaticinvokestatic這四條字節(jié)碼指令時(shí),如果類(lèi)型沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化階段。典型的Java代碼場(chǎng)景包括:
    • 使用new關(guān)鍵字實(shí)例化對(duì)象。
    • 讀取或設(shè)置一個(gè)類(lèi)型的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)。
    • 調(diào)用一個(gè)類(lèi)型的靜態(tài)方法。
  2. 使用java.lang.reflect包的方法對(duì)類(lèi)型進(jìn)行反射調(diào)用的時(shí)候,如果類(lèi)型沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化。
  3. 當(dāng)初始化類(lèi)的時(shí)候,如果發(fā)現(xiàn)其父類(lèi)還沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其父類(lèi)的初始化。
  4. 當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶(hù)需要指定一個(gè)要執(zhí)行的主類(lèi)(包含main()方法的那個(gè)類(lèi)),虛擬機(jī)會(huì)先初始化這個(gè)主類(lèi)。
  5. 當(dāng)使用JDK 7新加入的動(dòng)態(tài)語(yǔ)言支持時(shí),如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果為REF_getStaticREF_putStaticREF_invokeStaticREF_newInvokeSpecial四種類(lèi)型的方法句柄,并且這個(gè)方法句柄對(duì)應(yīng)的類(lèi)沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化。
  6. 當(dāng)一個(gè)接口中定義了JDK 8新加入的默認(rèn)方法(被default關(guān)鍵字修飾的接口方法)時(shí),如果有這個(gè)接口的實(shí)現(xiàn)類(lèi)發(fā)生了初始化,那該接口要在其之前被初始化。

這六種會(huì)觸發(fā)類(lèi)型進(jìn)行初始化的場(chǎng)景被稱(chēng)為主動(dòng)引用。除此之外,所有引用類(lèi)型的方式都不會(huì)觸發(fā)初始化,稱(chēng)為被動(dòng)引用

被動(dòng)引用

示例一

package algorithmAnalysis;
/** 
 * 被動(dòng)使用類(lèi)字段演示一:
 * 通過(guò)子類(lèi)引用父類(lèi)的靜態(tài)字段,不會(huì)導(dǎo)致子類(lèi)初始化 
 **/
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!");
    }
}

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

上述代碼中,運(yùn)行后只會(huì)輸出“SuperClass init!”而不會(huì)輸出“SubClass init!”。

這是因?yàn)閷?duì)于靜態(tài)字段,只有直接定義這個(gè)字段的類(lèi)才會(huì)被初始化。通過(guò)子類(lèi)引用父類(lèi)中定義的靜態(tài)字段,只會(huì)觸發(fā)父類(lèi)的初始化而不會(huì)觸發(fā)子類(lèi)的初始化。至于是否要觸發(fā)子類(lèi)的加載和驗(yàn)證階段,在《Java虛擬機(jī)規(guī)范》中并未明確規(guī)定,因此這一點(diǎn)取決于虛擬機(jī)的具體實(shí)現(xiàn)。在HotSpot虛擬機(jī)中,可以通過(guò)添加參數(shù)-XX:+TraceClassLoading觀(guān)察到這個(gè)操作會(huì)導(dǎo)致子類(lèi)加載,輸出結(jié)果如下。

示例二

package algorithmAnalysis;
/**
 * 通過(guò)數(shù)組定義來(lái)引用類(lèi),不會(huì)觸發(fā)此類(lèi)的初始化
 **/
public class Test {
    public static void main(String[] args) {
        SuperClass[] superClasses = new SuperClass[10];
    }
}

這段代碼復(fù)用了示例一的SuperClass,運(yùn)行之后發(fā)現(xiàn)沒(méi)有輸出“SuperClass init!”,說(shuō)明并沒(méi)有觸發(fā)類(lèi)algorithmAnalysis.SuperClass的初始化階段。但是這段代碼里面觸發(fā)了另一個(gè)名為“[LalgorithmAnalysis.SuperClass”的類(lèi)的初始化階段。

對(duì)于用戶(hù)代碼來(lái)說(shuō),這并不是一個(gè)合法的類(lèi)型名稱(chēng),它是一個(gè)由虛擬機(jī)自動(dòng)生成的、直接繼承于java.lang.Object的子類(lèi),創(chuàng)建動(dòng)作由
字節(jié)碼指令newarray觸發(fā)。

這個(gè)類(lèi)代表了一個(gè)元素類(lèi)型為algorithmAnalysis.SuperClass的一維數(shù)組,數(shù)組中應(yīng)有的屬性 和方法(用戶(hù)可直接使用的只有被修飾為public的length屬性和clone()方法)都實(shí)現(xiàn)在這個(gè)類(lèi)里。

Java語(yǔ) 言中對(duì)數(shù)組的訪(fǎng)問(wèn)要比C/C++相對(duì)安全,很大程度上就是因?yàn)檫@個(gè)類(lèi)包裝了數(shù)組元素的訪(fǎng)問(wèn)(準(zhǔn)確地說(shuō),越界檢查不是封裝在數(shù)組元素訪(fǎng)問(wèn)的類(lèi)中,而是封裝在數(shù)組訪(fǎng)問(wèn)的xaload、xastore字節(jié) 碼指令中),而 C/C++中則是直接翻譯為對(duì)數(shù)組指針的移動(dòng)。在Java語(yǔ)言里,當(dāng)檢查到發(fā)生數(shù)組越界時(shí)會(huì)拋出java.lang.ArrayIndexOutOfBoundsException異常,避免了直接造成非法內(nèi)存訪(fǎng)問(wèn)。

三、類(lèi)加載過(guò)程

3.1加載

在加載階段,Java虛擬機(jī)執(zhí)行以下三個(gè)主要任務(wù):

  1. 獲取二進(jìn)制字節(jié)流: 通過(guò)類(lèi)的全限定名獲取對(duì)應(yīng)的二進(jìn)制字節(jié)流,這是表示類(lèi)的靜態(tài)存儲(chǔ)結(jié)構(gòu)的基礎(chǔ)。
  2. 轉(zhuǎn)化為方法區(qū)數(shù)據(jù)結(jié)構(gòu): 將獲取的字節(jié)流表示的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。這包括對(duì)類(lèi)的字段、方法、接口等信息的整理和組織。
  3. 生成Class對(duì)象: 在內(nèi)存中創(chuàng)建一個(gè)java.lang.Class對(duì)象,用于在方法區(qū)中訪(fǎng)問(wèn)該類(lèi)的各種數(shù)據(jù)。這個(gè)Class對(duì)象是對(duì)類(lèi)的抽象,通過(guò)它可以獲取類(lèi)的各種信息。

《Java虛擬機(jī)規(guī)范》確實(shí)在對(duì)類(lèi)加載的過(guò)程中給予了相當(dāng)大的靈活性,沒(méi)有強(qiáng)制指定二進(jìn)制字節(jié)流必須從Class文件中獲取,這為Java虛擬機(jī)的實(shí)現(xiàn)和應(yīng)用帶來(lái)了廣泛的適用性和可擴(kuò)展性。開(kāi)發(fā)人員在這個(gè)靈活的舞臺(tái)上發(fā)揮了巨大的創(chuàng)造力,導(dǎo)致了許多重要的Java技術(shù)的誕生。以下是一些典型的應(yīng)用場(chǎng)景:

  • 從ZIP壓縮包中讀取: 這為日后JAR、EAR、WAR等格式的應(yīng)用打下了基礎(chǔ),這些格式在Java應(yīng)用中廣泛使用,提供了一種方便的打包和分發(fā)方式。
  • 從網(wǎng)絡(luò)中獲取: Web Applet是一個(gè)典型的應(yīng)用場(chǎng)景,它允許在Web瀏覽器中加載并執(zhí)行Java小程序,通過(guò)網(wǎng)絡(luò)獲取字節(jié)流。
  • 運(yùn)行時(shí)計(jì)算生成: 動(dòng)態(tài)代理技術(shù)是一個(gè)重要的應(yīng)用,它允許在運(yùn)行時(shí)生成代理類(lèi)的字節(jié)流,用于實(shí)現(xiàn)動(dòng)態(tài)代理。
  • 由其他文件生成: JSP應(yīng)用是一個(gè)例子,其中JSP文件會(huì)在運(yùn)行時(shí)被編譯成對(duì)應(yīng)的Class文件,實(shí)現(xiàn)了動(dòng)態(tài)生成和加載。
  • 從數(shù)據(jù)庫(kù)中讀取: 在一些中間件服務(wù)器中,程序代碼可以安裝到數(shù)據(jù)庫(kù)中,通過(guò)加載時(shí)從數(shù)據(jù)庫(kù)獲取相應(yīng)的字節(jié)流,實(shí)現(xiàn)了在集群間的分發(fā)。
  • 從加密文件中獲取: 采用加載時(shí)解密Class文件的方式,可以作為一種保護(hù)措施,防止Class文件被反編譯。

加載階段相對(duì)于類(lèi)加載過(guò)程的其他階段具有更高的可控性,尤其是在非數(shù)組類(lèi)型的加載階段。在這個(gè)階段,開(kāi)發(fā)人員可以通過(guò)以下方式靈活控制:

  1. 選擇類(lèi)加載器: 開(kāi)發(fā)人員可以選擇使用Java虛擬機(jī)內(nèi)置的引導(dǎo)類(lèi)加載器或自定義的類(lèi)加載器來(lái)完成加載階段。通過(guò)自定義類(lèi)加載器,可以根據(jù)需求控制字節(jié)流的獲取方式,例如重寫(xiě)類(lèi)加載器的findClass()loadClass()方法。
  2. 動(dòng)態(tài)獲取字節(jié)流: 在加載階段,開(kāi)發(fā)人員可以通過(guò)自定義類(lèi)加載器來(lái)動(dòng)態(tài)獲取類(lèi)的二進(jìn)制字節(jié)流。這為應(yīng)用程序提供了獲取運(yùn)行代碼的動(dòng)態(tài)性,開(kāi)發(fā)人員可以根據(jù)自己的需求實(shí)現(xiàn)字節(jié)流的獲取邏輯。

對(duì)于數(shù)組類(lèi)的加載,雖然數(shù)組類(lèi)本身是由Java虛擬機(jī)直接在內(nèi)存中動(dòng)態(tài)構(gòu)造的,但與類(lèi)加載器仍然存在密切關(guān)系。數(shù)組類(lèi)的創(chuàng)建遵循以下規(guī)則:

  • 如果數(shù)組的組件類(lèi)型是引用類(lèi)型,遞歸采用加載過(guò)程加載組件類(lèi)型,數(shù)組類(lèi)將被標(biāo)識(shí)在加載該組件類(lèi)型的類(lèi)加載器的類(lèi)名稱(chēng)空間上。
  • 如果數(shù)組的組件類(lèi)型不是引用類(lèi)型,數(shù)組類(lèi)將被標(biāo)記為與引導(dǎo)類(lèi)加載器關(guān)聯(lián)。

此外,數(shù)組類(lèi)的可訪(fǎng)問(wèn)性與其組件類(lèi)型的可訪(fǎng)問(wèn)性一致。如果組件類(lèi)型不是引用類(lèi)型,數(shù)組類(lèi)的可訪(fǎng)問(wèn)性默認(rèn)為public,可被所有的類(lèi)和接口訪(fǎng)問(wèn)。

加載階段結(jié)束后,二進(jìn)制字節(jié)流按虛擬機(jī)設(shè)定的格式存儲(chǔ)在方法區(qū)中。在方法區(qū)中,類(lèi)型數(shù)據(jù)會(huì)被實(shí)例化為一個(gè)java.lang.Class對(duì)象,這個(gè)對(duì)象作為程序訪(fǎng)問(wèn)方法區(qū)中類(lèi)型數(shù)據(jù)的外部接口。

需要注意的是,加載階段與連接階段的一些動(dòng)作是交叉進(jìn)行的,加載階段尚未完成,連接階段可能已經(jīng)開(kāi)始。這兩個(gè)階段的開(kāi)始時(shí)間保持著固定的先后順序。

3.2驗(yàn)證

Java語(yǔ)言本身具有相對(duì)較高的安全性,相比于C/C++等語(yǔ)言來(lái)說(shuō)更為安全。使用純粹的Java代碼通常無(wú)法執(zhí)行一些危險(xiǎn)操作,比如訪(fǎng)問(wèn)數(shù)組邊界以外的數(shù)據(jù)、將對(duì)象轉(zhuǎn)型為其未實(shí)現(xiàn)的類(lèi)型、跳轉(zhuǎn)到不存在的代碼行等。在這些情況下,編譯器會(huì)嚴(yán)格拋出異常并拒絕編譯。

然而,需要注意的是,Class文件并不一定只能由Java源碼編譯而來(lái)。任何途徑產(chǎn)生的Class文件,包括直接在二進(jìn)制編輯器中編寫(xiě)0和1的方式,都是有效的。因此,驗(yàn)證字節(jié)碼是Java虛擬機(jī)保護(hù)自身安全的必要措施。

驗(yàn)證階段在整個(gè)類(lèi)加載過(guò)程中具有重要意義,其嚴(yán)謹(jǐn)程度直接影響Java虛擬機(jī)是否能夠抵御惡意代碼攻擊。驗(yàn)證階段涵蓋了文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證和符號(hào)引用驗(yàn)證等四個(gè)主要方面。

  1. 文件格式驗(yàn)證: 驗(yàn)證Class文件是否符合Java虛擬機(jī)規(guī)定的文件格式標(biāo)準(zhǔn)。
  2. 元數(shù)據(jù)驗(yàn)證: 確保類(lèi)的元數(shù)據(jù)信息符合規(guī)范,包括類(lèi)的繼承關(guān)系、字段和方法的聲明等。
  3. 字節(jié)碼驗(yàn)證: 對(duì)字節(jié)碼進(jìn)行驗(yàn)證,防止惡意代碼通過(guò)字節(jié)碼漏洞對(duì)系統(tǒng)進(jìn)行攻擊。
  4. 符號(hào)引用驗(yàn)證: 確保類(lèi)在運(yùn)行時(shí)能夠正確鏈接到其他類(lèi),并且這些類(lèi)存在并具有正確的權(quán)限。

驗(yàn)證階段的工作量相當(dāng)大,涉及到整個(gè)類(lèi)加載過(guò)程的安全性和性能。因此,它是保障Java應(yīng)用程序安全執(zhí)行的關(guān)鍵環(huán)節(jié)。

文件格式驗(yàn)證

在驗(yàn)證階段的第一階段,主要任務(wù)是驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能夠被當(dāng)前版本的虛擬機(jī)正確處理。以下是包含在這一階段的驗(yàn)證點(diǎn):

  • 魔數(shù)驗(yàn)證: 檢查Class文件是否以魔數(shù)0xCAFEBABE開(kāi)頭,這是Java Class文件的標(biāo)識(shí)。
  • 版本號(hào)驗(yàn)證: 確保主版本號(hào)和次版本號(hào)是否在當(dāng)前Java虛擬機(jī)接受的范圍之內(nèi)。
  • 常量池驗(yàn)證: 檢查常量池中的常量類(lèi)型是否被當(dāng)前虛擬機(jī)支持,包括檢查常量的tag標(biāo)志。
  • 索引值驗(yàn)證: 確保指向常量的各種索引值沒(méi)有指向不存在的常量,而且索引值的類(lèi)型符合常量的類(lèi)型。
  • UTF-8編碼驗(yàn)證: 對(duì)于CONSTANT_Utf8_info型的常量,檢查其中的數(shù)據(jù)是否符合UTF-8編碼規(guī)范。
  • 文件結(jié)構(gòu)驗(yàn)證: 確保Class文件中各個(gè)部分以及文件本身沒(méi)有被刪除的或附加的其他信息,保持結(jié)構(gòu)的完整性。

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

在驗(yàn)證階段的第二階段,主要任務(wù)是對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,以確保其描述的信息符合《Java語(yǔ)言規(guī)范》的要求。以下是包含在這一階段的驗(yàn)證點(diǎn):

  • 父類(lèi)驗(yàn)證: 確保每個(gè)類(lèi)除了java.lang.Object之外都應(yīng)該有父類(lèi)。
  • 繼承驗(yàn)證: 檢查父類(lèi)是否繼承了不允許被繼承的類(lèi),即被final修飾的類(lèi)。
  • 接口實(shí)現(xiàn)驗(yàn)證: 如果一個(gè)類(lèi)不是抽象類(lèi),確保它實(shí)現(xiàn)了其父類(lèi)或接口中要求實(shí)現(xiàn)的所有方法。
  • 字段和方法驗(yàn)證: 檢查類(lèi)中的字段和方法是否與父類(lèi)產(chǎn)生矛盾,例如覆蓋了父類(lèi)的final字段,或者出現(xiàn)不符合規(guī)則的方法重載(方法參數(shù)一致但返回值類(lèi)型不同等)。

這些驗(yàn)證點(diǎn)旨在對(duì)類(lèi)的元數(shù)據(jù)信息進(jìn)行語(yǔ)義校驗(yàn),以確保它們符合Java語(yǔ)言規(guī)范的定義。

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

在驗(yàn)證階段的第三階段,通過(guò)數(shù)據(jù)流分析和控制流分析,目標(biāo)是確定程序語(yǔ)義是合法的、符合邏輯的。在進(jìn)行方法體的校驗(yàn)分析時(shí),主要考慮以下驗(yàn)證點(diǎn):

  1. 操作數(shù)棧和指令代碼協(xié)同工作: 確保任何時(shí)刻操作數(shù)棧的數(shù)據(jù)類(lèi)型與指令代碼序列配合工作,防止出現(xiàn)操作棧放置了一個(gè)數(shù)據(jù)類(lèi)型,使用時(shí)按不同類(lèi)型加載入本地變量表的情況。
  2. 跳轉(zhuǎn)指令的合法性: 保證任何跳轉(zhuǎn)指令都不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令。
  3. 類(lèi)型轉(zhuǎn)換的有效性: 確保方法體中的類(lèi)型轉(zhuǎn)換總是有效的,例如可以將子類(lèi)對(duì)象賦值給父類(lèi)數(shù)據(jù)類(lèi)型,而將父類(lèi)對(duì)象賦值給子類(lèi)數(shù)據(jù)類(lèi)型是危險(xiǎn)和不合法的。

為了降低在字節(jié)碼驗(yàn)證階段中的執(zhí)行時(shí)間開(kāi)銷(xiāo),Java虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)采用了聯(lián)合優(yōu)化策略。該策略在JDK 6之后實(shí)施,主要包括在Javac編譯器中增加了校驗(yàn)輔助措施,并通過(guò)引入名為StackMapTable的新屬性來(lái)描述方法體的基本塊狀態(tài)。這一策略的核心思想是通過(guò)在編譯期執(zhí)行盡可能多的校驗(yàn)輔助措施,從而減輕字節(jié)碼驗(yàn)證期間的負(fù)擔(dān)。具體而言:

  • Javac編譯器中的校驗(yàn)輔助措施: Javac編譯器在編譯期執(zhí)行一系列校驗(yàn)輔助措施,以便在方法體的Code屬性中引入StackMapTable屬性。這樣,編譯器在校驗(yàn)階段就能夠提供關(guān)于基本塊狀態(tài)的信息,減輕虛擬機(jī)在字節(jié)碼驗(yàn)證期間的工作。
  • StackMapTable屬性的引入: StackMapTable屬性是一項(xiàng)新的屬性,用于描述方法體的基本塊狀態(tài)。這個(gè)屬性記錄了基本塊開(kāi)始時(shí)本地變量表和操作棧應(yīng)有的狀態(tài)。虛擬機(jī)在字節(jié)碼驗(yàn)證期間只需檢查StackMapTable屬性中的記錄是否合法,而無(wú)需推導(dǎo)這些狀態(tài)的合法性,從而減少了驗(yàn)證的時(shí)間開(kāi)銷(xiāo)。

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

在類(lèi)加載的最后一個(gè)階段,校驗(yàn)行為發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候,即發(fā)生在連接的第三階段——解析階段中。符號(hào)引用驗(yàn)證旨在匹配類(lèi)自身以外的各類(lèi)信息,確保類(lèi)能夠正常訪(fǎng)問(wèn)其依賴(lài)的外部類(lèi)、方法、字段等資源。此階段通常需要校驗(yàn)以下內(nèi)容:

  1. 符號(hào)引用中通過(guò)字符串描述的全限定名是否能找到對(duì)應(yīng)的類(lèi)。
  2. 指定類(lèi)中是否存在符合方法的字段描述符及簡(jiǎn)單名稱(chēng)所描述的方法和字段。
  3. 符號(hào)引用中的類(lèi)、字段、方法的可訪(fǎng)問(wèn)性(private、protected、public、)是否可被當(dāng)前類(lèi)訪(fǎng)問(wèn)。

符號(hào)引用驗(yàn)證的主要目的是確保解析行為能夠正常執(zhí)行。如果無(wú)法通過(guò)符號(hào)引用驗(yàn)證,Java虛擬機(jī)將會(huì)拋出一個(gè)java.lang.IncompatibleClassChangeError的子類(lèi)異常,如java.lang.IllegalAccessErrorjava.lang.NoSuchFieldErrorjava.lang.NoSuchMethodError等。

驗(yàn)證階段對(duì)于虛擬機(jī)的類(lèi)加載機(jī)制是重要但非強(qiáng)制執(zhí)行的階段,因?yàn)轵?yàn)證階段只有通過(guò)或不通過(guò)的差別。一旦通過(guò)了驗(yàn)證,其后對(duì)程序運(yùn)行期沒(méi)有任何影響。

如果程序運(yùn)行的全部代碼(包括自己編寫(xiě)的、第三方包中的、從外部加載的、動(dòng)態(tài)生成的等所有代碼)都經(jīng)過(guò)了反復(fù)使用和驗(yàn)證,那么在生產(chǎn)環(huán)境的實(shí)施階段可以考慮使用-Xverify:none參數(shù)關(guān)閉大部分的類(lèi)驗(yàn)證措施,以縮短虛擬機(jī)類(lèi)加載的時(shí)間。

3.3準(zhǔn)備

準(zhǔn)備階段是正式為類(lèi)中定義的變量(即靜態(tài)變量,被static修飾的變量)分配內(nèi)存并設(shè)置類(lèi)變量初始值的階段

這時(shí)候進(jìn)行內(nèi)存分配的僅包括類(lèi)變量,而不包括實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在Java堆中。

初始值通常情況下是數(shù)據(jù)類(lèi)型的零值:

  • public static int value = 123;
    • 準(zhǔn)備后為 0,value 的賦值指令 putstatic 會(huì)被放在 <clinit>() 方法中,<clinit>()方法會(huì)在初始化時(shí)執(zhí)行,也就是說(shuō),value 變量只有在初始化后才等于 123。

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

  • public static final int value = 123;()
    • 準(zhǔn)備后為 123,因?yàn)楸?static final 賦值之后 value 就不能再修改了,所以在這里進(jìn)行了賦值之后,之后不可能再出現(xiàn)賦值操作,所以可以直接在準(zhǔn)備階段就把 value 的值初始化好。

3.4解析

解析階段是Java虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程,JVM學(xué)習(xí)-Class文件結(jié)構(gòu)-符號(hào)引用

  • 在此之前,常量池中的引用是不一定存在的,解析過(guò)之后,可以保證常量池中的引用在內(nèi)存中一定存在。
  • 什么是 “符號(hào)引用” 和 “直接引用” ?
    • 符號(hào)引用:以一組符號(hào)描述所引用的對(duì)象(如對(duì)象的全類(lèi)名),引用的目標(biāo)不一定存在于內(nèi)存中。
    • 直接引用:直接指向被引用目標(biāo)在內(nèi)存中的位置的指針等,也就是說(shuō),引用的目標(biāo)一定存在于內(nèi)存中。

對(duì)同一個(gè)符號(hào)引用進(jìn)行多次解析請(qǐng)求是很常見(jiàn)的事情,除invokedynamic指令以外,虛擬機(jī)實(shí)現(xiàn)可以對(duì)第一次解析的結(jié)果進(jìn)行緩存,譬如在運(yùn)行時(shí)直接引用常量池中的記錄,并把常量標(biāo)識(shí)為已解析狀態(tài),從而避免解析動(dòng)作重復(fù)進(jìn)行。

invokedynamic指令的目的本來(lái)就是用于動(dòng)態(tài)語(yǔ)言支持,它對(duì)應(yīng)的引用稱(chēng)為“動(dòng)態(tài)調(diào)用點(diǎn)限定符 (Dynamically-Computed Call Site Specifier)”,這里“動(dòng)態(tài)”的含義是指必須等到程序?qū)嶋H運(yùn)行到這條指令時(shí),解析動(dòng)作才能進(jìn)行。
相對(duì)地,其余可觸發(fā)解析的指令都是“靜態(tài)”的,可以在剛剛完成加載階段,還沒(méi)有開(kāi)始執(zhí)行代碼時(shí)就提前進(jìn)行解析。

類(lèi)或接口的解析

假設(shè)當(dāng)前代碼所處的類(lèi)為D,解析一個(gè)從未解析過(guò)的符號(hào)引用N為一個(gè)類(lèi)或接口C的直接引用通常涉及以下三個(gè)步驟:

  1. 加載類(lèi)C: 如果C不是一個(gè)數(shù)組類(lèi)型,虛擬機(jī)將把代表N的全限定名傳遞給當(dāng)前類(lèi)D的類(lèi)加載器,以加載這個(gè)類(lèi)C。在加載的過(guò)程中,可能會(huì)觸發(fā)元數(shù)據(jù)驗(yàn)證和字節(jié)碼驗(yàn)證,也可能導(dǎo)致其他相關(guān)類(lèi)的加載,例如加載C的父類(lèi)或?qū)崿F(xiàn)的接口。如果加載過(guò)程中出現(xiàn)異常,解析過(guò)程失敗。
  2. 加載數(shù)組元素類(lèi)型(如果C是數(shù)組類(lèi)型): 如果C是一個(gè)數(shù)組類(lèi)型,而且數(shù)組的元素類(lèi)型是對(duì)象類(lèi)型,那么N的描述符將是類(lèi)似于"[Ljava/lang/Integer"的形式。虛擬機(jī)將按照第一步的規(guī)則加載數(shù)組元素類(lèi)型。如果N的描述符是類(lèi)似于"java.lang.Integer"的形式,虛擬機(jī)生成一個(gè)代表該數(shù)組維度和元素類(lèi)型的數(shù)組對(duì)象。
  3. 符號(hào)引用驗(yàn)證和訪(fǎng)問(wèn)權(quán)限檢查: 如果前兩步?jīng)]有異常,那么C在虛擬機(jī)中已經(jīng)成為一個(gè)有效的類(lèi)或接口。在解析完成前,需要進(jìn)行符號(hào)引用驗(yàn)證,以確認(rèn)當(dāng)前類(lèi)D是否具有對(duì)C的訪(fǎng)問(wèn)權(quán)限。如果訪(fǎng)問(wèn)權(quán)限驗(yàn)證失敗,將拋出java.lang.IllegalAccessError異常。在JDK 9及之后的版本中,需要考慮模塊化的因素,即訪(fǎng)問(wèn)權(quán)限驗(yàn)證還需檢查模塊之間的訪(fǎng)問(wèn)權(quán)限。具體來(lái)說(shuō),一個(gè)D要訪(fǎng)問(wèn)C,至少滿(mǎn)足以下三條規(guī)則之一:
    • 被訪(fǎng)問(wèn)的類(lèi)C是public的,并且與訪(fǎng)問(wèn)類(lèi)D處于同一個(gè)模塊。
    • 被訪(fǎng)問(wèn)的類(lèi)C是public的,不與訪(fǎng)問(wèn)類(lèi)D處于同一個(gè)模塊,但是被訪(fǎng)問(wèn)類(lèi)C的模塊允許被訪(fǎng)問(wèn)類(lèi)D的模塊進(jìn)行訪(fǎng)問(wèn)。
    • 被訪(fǎng)問(wèn)的類(lèi)C不是public的,但是它與訪(fǎng)問(wèn)類(lèi)D處于同一個(gè)包中。

字段解析

要解析一個(gè)未被解析過(guò)的字段符號(hào)引用,通常需要按照以下步驟進(jìn)行:

  1. 解析類(lèi)或接口符號(hào)引用: 對(duì)字段表內(nèi)class_index項(xiàng)(class_index)中索引的CONSTANT_Class_info符號(hào)引用進(jìn)行解析,即解析字段所屬的類(lèi)或接口的符號(hào)引用。如果在解析這個(gè)類(lèi)或接口符號(hào)引用的過(guò)程中出現(xiàn)異常,導(dǎo)致字段符號(hào)引用解析失敗。
  2. 后續(xù)字段搜索: 如果類(lèi)或接口符號(hào)引用解析成功,用C表示這個(gè)字段所屬的類(lèi)或接口。按照《Java虛擬機(jī)規(guī)范》的規(guī)定,對(duì)C進(jìn)行后續(xù)字段搜索:
    • 如果C本身包含了簡(jiǎn)單名稱(chēng)和字段描述符都與目標(biāo)相匹配的字段,返回這個(gè)字段的直接引用,搜索結(jié)束。
    • 否則,如果在C中實(shí)現(xiàn)了接口,按照繼承關(guān)系從下往上遞歸搜索各個(gè)接口和它們的父接口,如果接口中包含了簡(jiǎn)單名稱(chēng)和字段描述符都與目標(biāo)相匹配的字段,返回這個(gè)字段的直接引用,搜索結(jié)束。
    • 否則,如果C不是java.lang.Object,按照繼承關(guān)系從下往上遞歸搜索其父類(lèi),如果在父類(lèi)中包含了簡(jiǎn)單名稱(chēng)和字段描述符都與目標(biāo)相匹配的字段,返回這個(gè)字段的直接引用,搜索結(jié)束。
    • 否則,查找失敗,拋出java.lang.NoSuchFieldError異常。
  3. 權(quán)限驗(yàn)證: 如果查找成功返回了引用,對(duì)這個(gè)字段進(jìn)行權(quán)限驗(yàn)證。如果發(fā)現(xiàn)不具備對(duì)字段的訪(fǎng)問(wèn)權(quán)限,拋出java.lang.IllegalAccessError異常。

解析規(guī)則確保Java虛擬機(jī)能夠獲得字段的唯一解析結(jié)果。在實(shí)際情況中,Javac編譯器可能會(huì)采取比規(guī)范更加嚴(yán)格的約束,例如,當(dāng)一個(gè)同名字段同時(shí)出現(xiàn)在某個(gè)類(lèi)的接口和父類(lèi)中,或者同時(shí)在自己或父類(lèi)的多個(gè)接口中出現(xiàn)時(shí),Javac編譯器可能會(huì)拒絕編譯為Class文件。

方法解析

方法解析的步驟與字段解析相似,通常包括以下步驟:

  1. 解析類(lèi)或接口符號(hào)引用: 首先,需要解析方法表的class_index項(xiàng)中索引的方法所屬的類(lèi)或接口的符號(hào)引用。使用C表示這個(gè)類(lèi)。如果解析成功,繼續(xù)后續(xù)的方法搜索。
  2. 接口檢查: 如果在類(lèi)的方法表中發(fā)現(xiàn)class_index中索引的C是個(gè)接口,直接拋出java.lang.IncompatibleClassChangeError異常。
  3. 后續(xù)方法搜索: 根據(jù)以下步驟進(jìn)行后續(xù)的方法搜索:
    • 在類(lèi)C中查找是否有簡(jiǎn)單名稱(chēng)和描述符都與目標(biāo)相匹配的方法,如果有則返回這個(gè)方法的直接引用,搜索結(jié)束。
    • 否則,在類(lèi)C的父類(lèi)中遞歸查找是否有簡(jiǎn)單名稱(chēng)和描述符都與目標(biāo)相匹配的方法,如果有則返回這個(gè)方法的直接引用,搜索結(jié)束。
    • 否則,在類(lèi)C實(shí)現(xiàn)的接口列表及它們的父接口之中遞歸查找是否有簡(jiǎn)單名稱(chēng)和描述符都與目標(biāo)相匹配的方法,如果存在匹配的方法,說(shuō)明類(lèi)C是一個(gè)抽象類(lèi),查找結(jié)束,拋出java.lang.AbstractMethodError異常。
    • 否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError
  4. 權(quán)限驗(yàn)證: 如果查找過(guò)程成功返回了直接引用,對(duì)這個(gè)方法進(jìn)行權(quán)限驗(yàn)證。如果發(fā)現(xiàn)不具備對(duì)此方法的訪(fǎng)問(wèn)權(quán)限,拋出java.lang.IllegalAccessError異常。

接口方法解析

解析接口方法和解析類(lèi)的方法在主要邏輯上是相似的,但由于接口和類(lèi)在Java中有一些不同的特性,導(dǎo)致在解析過(guò)程中存在一些細(xì)微的差異:

  1. 類(lèi)型檢查: 在解析接口方法時(shí),需要進(jìn)行接口類(lèi)型檢查。如果接口方法表中發(fā)現(xiàn)所屬的類(lèi)(class_index中索引的C)是個(gè)類(lèi)而不是接口,會(huì)直接拋出java.lang.IncompatibleClassChangeError異常。這是因?yàn)榻涌诜椒ū仨殞儆诮涌冢荒苁穷?lèi)的方法。
  2. 搜索范圍: 解析類(lèi)的方法時(shí),只需要在類(lèi)本身及其父類(lèi)中查找匹配的方法。而解析接口方法時(shí),需要在接口本身及其所有父接口中遞歸查找。這是因?yàn)镴ava接口支持多重繼承,一個(gè)接口可以繼承多個(gè)父接口的方法。
  3. 多重繼承處理: 對(duì)于可能存在多個(gè)父接口中有相匹配的方法的情況,解析接口方法時(shí)可以從中選擇一個(gè)并返回。這一點(diǎn)與解析類(lèi)的方法不同,因?yàn)轭?lèi)只有一個(gè)直接的父類(lèi),不存在多重繼承的情況。

在JDK 9之前,Java接口中的所有方法默認(rèn)都是public的,且不存在模塊化的訪(fǎng)問(wèn)約束,因此接口方法的符號(hào)解析不會(huì)拋出java.lang.IllegalAccessError異常。然而,從JDK 9開(kāi)始,引入了接口的靜態(tài)私有方法以及模塊化的訪(fǎng)問(wèn)約束,因此在JDK 9及以后的版本中,接口方法的訪(fǎng)問(wèn)可能會(huì)因?yàn)樵L(fǎng)問(wèn)權(quán)限控制而拋出java.lang.IllegalAccessError異常。

3.5初始化

在Java虛擬機(jī)的類(lèi)加載過(guò)程中,初始化階段是加載過(guò)程的最后一個(gè)步驟。在初始化階段,Java虛擬機(jī)執(zhí)行類(lèi)構(gòu)造器<clinit>()方法,該方法是由編譯器自動(dòng)生成的,用于執(zhí)行類(lèi)中的所有類(lèi)變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊中的語(yǔ)句

以下是關(guān)于初始化階段和<clinit>()方法的一些重要信息:

  • <clinit>()方法的生成: <clinit>()方法由編譯器自動(dòng)收集類(lèi)中的所有類(lèi)變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊中的語(yǔ)句生成。編譯器根據(jù)源文件中語(yǔ)句的順序來(lái)確定收集的順序。
  • 靜態(tài)語(yǔ)句塊的順序: 在靜態(tài)語(yǔ)句塊中,只能訪(fǎng)問(wèn)到定義在靜態(tài)語(yǔ)句塊之前的變量。靜態(tài)語(yǔ)句塊中可以賦值但不能訪(fǎng)問(wèn)定義在其后的變量。
  • <clinit>()與<init>()的區(qū)別: <clinit>()方法與類(lèi)的構(gòu)造函數(shù)(實(shí)例構(gòu)造器<init>()方法)不同。它不需要顯式調(diào)用父類(lèi)構(gòu)造器,并且Java虛擬機(jī)保證在子類(lèi)的<clinit>()方法執(zhí)行前,父類(lèi)的<clinit>()方法已經(jīng)執(zhí)行完畢。第一個(gè)被執(zhí)行的<clinit>()方法的類(lèi)型是java.lang.Object
  • 接口中的<clinit>(): 接口中不能使用靜態(tài)語(yǔ)句塊,但仍然會(huì)有變量初始化的賦值操作,因此接口也會(huì)生成<clinit>()方法。與類(lèi)不同的是,執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父接口的<clinit>()方法。
  • 多線(xiàn)程初始化: Java虛擬機(jī)必須保證一個(gè)類(lèi)的<clinit>()方法在多線(xiàn)程環(huán)境中正確地加鎖同步。如果多個(gè)線(xiàn)程同時(shí)初始化一個(gè)類(lèi),只會(huì)有其中一個(gè)線(xiàn)程執(zhí)行該類(lèi)的<clinit>()方法,其他線(xiàn)程需要阻塞等待。
  • 初始化階段觸發(fā)時(shí)機(jī): 初始化階段的觸發(fā)時(shí)機(jī)包括對(duì)類(lèi)的主動(dòng)使用,如創(chuàng)建類(lèi)的實(shí)例、調(diào)用類(lèi)的靜態(tài)方法、訪(fǎng)問(wèn)類(lèi)或接口的靜態(tài)字段等。只有在對(duì)類(lèi)進(jìn)行主動(dòng)使用時(shí),初始化階段才會(huì)被觸發(fā)。
  • 線(xiàn)程安全性: 在多線(xiàn)程環(huán)境中,如果多個(gè)線(xiàn)程同時(shí)嘗試初始化同一個(gè)類(lèi),Java虛擬機(jī)會(huì)確保只有一個(gè)線(xiàn)程執(zhí)行該類(lèi)的<clinit>()方法,其他線(xiàn)程需要等待。

四、類(lèi)加載器

Java虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)采用創(chuàng)新的方式將類(lèi)加載階段中獲取類(lèi)的二進(jìn)制字節(jié)流的動(dòng)作放到Java虛擬機(jī)外部實(shí)現(xiàn),這個(gè)實(shí)現(xiàn)被稱(chēng)為"類(lèi)加載器"(Class Loader)。這設(shè)計(jì)的初衷是為了讓?xiě)?yīng)用程序自己決定如何獲取所需的類(lèi),為Java語(yǔ)言帶來(lái)了靈活性和可擴(kuò)展性。

4.1類(lèi)與類(lèi)加載器

類(lèi)加載器雖然只用于實(shí)現(xiàn)類(lèi)的加載動(dòng)作,但它在Java程序中起到的作用卻遠(yuǎn)超類(lèi)加載階段。對(duì)于任意一個(gè)類(lèi),都必須由加載它的類(lèi)加載器和這個(gè)類(lèi)本身一起共同確立其在Java虛擬機(jī)中的唯一性,每一個(gè)類(lèi)加載器,都擁有一個(gè)獨(dú)立的類(lèi)名稱(chēng)空間。

如何判斷兩個(gè)類(lèi) “相等”?

  • “相等” 的要求
    • 同一個(gè) .class 文件
    • 被同一個(gè)虛擬機(jī)加載
    • 被同一個(gè)類(lèi)加載器加載
  • 判斷 “相等” 的方法
    • instanceof 關(guān)鍵字
    • Class 對(duì)象中的方法:
      • equals()
      • isInstance()
      • isAssignableFrom()

4.2類(lèi)加載器分類(lèi)

Java虛擬機(jī)的角度來(lái)看,只存在兩種不同的類(lèi)加載器:

  • 啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader),這個(gè)類(lèi)加載器使用C++語(yǔ)言實(shí)現(xiàn),是虛擬機(jī)自身的一部分
  • 其他所有的類(lèi)加載器,這些類(lèi)加載器都由Java語(yǔ)言實(shí)現(xiàn),獨(dú)立存在于虛擬機(jī)外部,并且全都繼承自抽象類(lèi)java.lang.ClassLoader

Java開(kāi)發(fā)人員的角度來(lái)看,類(lèi)加載器就應(yīng)當(dāng)劃分得更細(xì)致一些。三層類(lèi)加載器、雙親委派的類(lèi)加載架構(gòu):

  • 啟動(dòng)類(lèi)加載器(Bootstrap):
    • 是Java虛擬機(jī)能夠識(shí)別的,按照文件名識(shí)別,如rt.jar、tools.jar,名字不符合的類(lèi)庫(kù)即使放在lib目錄中也不會(huì)被加載
    • <JAVA_HOME>/lib
    • -Xbootclasspath 參數(shù)指定的路徑
  • 擴(kuò)展類(lèi)加載器(Extension)
    • <JAVA_HOME>/lib/ext
    • java.ext.dirs 系統(tǒng)變量指定的路徑
  • 應(yīng)用程序類(lèi)加載器(Application)/系統(tǒng)類(lèi)加載器
    • 負(fù)責(zé)加載用戶(hù)類(lèi)路徑 (ClassPath)上所有的類(lèi)庫(kù),開(kāi)發(fā)者同樣可以直接在代碼中使用這個(gè)類(lèi)加載器。如果應(yīng)用程序中沒(méi)有
      自定義過(guò)自己的類(lèi)加載器,一般情況下這個(gè)就是程序中默認(rèn)的類(lèi)加載器
    • -classpath 參數(shù)

4.3雙親委派模型

圖7中展示的各種類(lèi)加載器之間的層次關(guān)系被稱(chēng)為類(lèi)加載器的“雙親委派模型(Parents Delegation Model)”。

雙親委派模型要求除了頂層的啟動(dòng)類(lèi)加載器外,其余的類(lèi)加載器都應(yīng)有自己的父類(lèi)加載器。不過(guò)這里類(lèi)加載器之間的父子關(guān)系一般不是以繼承(Inheritance)的關(guān)系來(lái)實(shí)現(xiàn)的,而是通常使用組合(Composition)關(guān)系來(lái)復(fù)用父加載器的代碼。

工作過(guò)程

  • 當(dāng)前類(lèi)加載器收到類(lèi)加載的請(qǐng)求后,先不自己嘗試加載類(lèi),而是先將請(qǐng)求委派給父類(lèi)加載器

    因此,所有的類(lèi)加載請(qǐng)求,都會(huì)先被傳送到啟動(dòng)類(lèi)加載器

  • 只有當(dāng)父類(lèi)加載器加載失敗時(shí),當(dāng)前類(lèi)加載器才會(huì)嘗試自己去自己負(fù)責(zé)的區(qū)域加載

實(shí)現(xiàn)

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 首先,檢查請(qǐng)求的類(lèi)是否已經(jīng)被加載過(guò)了
    Class<?> c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // 如果父類(lèi)加載器拋出ClassNotFoundException
            // 說(shuō)明父類(lèi)加載器無(wú)法完成加載請(qǐng)求
        }
        
        if (c == null) {
            // 在父類(lèi)加載器無(wú)法加載時(shí)
            // 再調(diào)用本身的findClass方法來(lái)進(jìn)行類(lèi)加載
            c = findClass(name);
        }
    }

    if (resolve) {
        resolveClass(c);
    }

    return c;
}
  • 檢查該類(lèi)是否已經(jīng)被加載
  • 將類(lèi)加載請(qǐng)求委派給父類(lèi)
    • 如果父類(lèi)加載器為 null,默認(rèn)使用啟動(dòng)類(lèi)加載器
    • parent.loadClass(name, false)
  • 當(dāng)父類(lèi)加載器加載失敗時(shí)
    • catch ClassNotFoundException 但不做任何處理
    • 調(diào)用自己的 findClass() 去加載
      • 我們?cè)趯?shí)現(xiàn)自己的類(lèi)加載器時(shí)只需要 extends ClassLoader,然后重寫(xiě) findClass() 方法而不是 loadClass() 方法,這樣就不用重寫(xiě) loadClass() 中的雙親委派機(jī)制了

優(yōu)點(diǎn)

  • 避免重復(fù)加載: 雙親委派機(jī)制通過(guò)委派給父類(lèi)加載器來(lái)嘗試加載類(lèi),可以避免同樣的類(lèi)被多次加載。如果一個(gè)類(lèi)已經(jīng)被一個(gè)類(lèi)加載器加載,那么其父加載器會(huì)首先被詢(xún)問(wèn)是否能夠加載這個(gè)類(lèi),從而避免了重復(fù)加載,提高了類(lèi)加載的效率。
  • 安全性: 雙親委派機(jī)制可以提高類(lèi)加載的安全性。由于類(lèi)加載是從上往下委派的,父加載器加載的類(lèi)能夠保證在整個(gè)加載層次結(jié)構(gòu)中是唯一的,這有助于防止惡意類(lèi)的加載和替代。
  • 保護(hù)核心類(lèi)庫(kù): 雙親委派機(jī)制確保核心類(lèi)庫(kù)(如java.langjava.util等)由啟動(dòng)類(lèi)加載器加載,防止用戶(hù)自定義類(lèi)替代核心類(lèi)庫(kù),從而保護(hù)了Java運(yùn)行環(huán)境的穩(wěn)定性和一致性。
  • 模塊化: 雙親委派機(jī)制有助于實(shí)現(xiàn)模塊化。通過(guò)層級(jí)結(jié)構(gòu)和委派機(jī)制,類(lèi)加載器可以根據(jù)不同的需求劃分加載的范圍,形成模塊化的結(jié)構(gòu)。

破壞雙親委派機(jī)制

在某些情況下,開(kāi)發(fā)者可能會(huì)有意或無(wú)意地破壞雙親委派機(jī)制。以下是一些可能導(dǎo)致雙親委派機(jī)制破壞的情況:

  1. 自定義類(lèi)加載器: 開(kāi)發(fā)者可以通過(guò)自定義類(lèi)加載器來(lái)加載類(lèi),而自定義類(lèi)加載器可以選擇性地打破雙親委派機(jī)制。例如,覆蓋 loadClass 方法時(shí),可以選擇不調(diào)用父類(lèi)加載器的 loadClass 方法,從而實(shí)現(xiàn)自定義的加載邏輯。
  2. 線(xiàn)程上下文類(lèi)加載器: Java中的線(xiàn)程上下文類(lèi)加載器(Context Class Loader)可以通過(guò) Thread.setContextClassLoader 方法進(jìn)行設(shè)置。在一些框架和應(yīng)用場(chǎng)景中,開(kāi)發(fā)者可能會(huì)為線(xiàn)程設(shè)置上下文類(lèi)加載器,以便在特定的情況下改變類(lèi)加載器的委派行為。
  3. Java Instrumentation API: Java提供了 Instrumentation API,允許開(kāi)發(fā)者在類(lèi)加載的過(guò)程中進(jìn)行字節(jié)碼的修改。通過(guò)在 premain 方法中使用 java.lang.instrument.ClassFileTransformer 接口,開(kāi)發(fā)者可以修改類(lèi)的字節(jié)碼,從而破壞雙親委派機(jī)制。
  4. 模塊化中平臺(tái)類(lèi)加載器優(yōu)先委派給負(fù)責(zé)那個(gè)模塊的加載器完成加載

五、Java模塊化系統(tǒng)

在Java 9之前,Java應(yīng)用程序是以JAR文件的形式組織的,其中包含了一堆類(lèi)和資源。這種方式存在一些問(wèn)題:

  • 可維護(hù)性差:JAR文件可以包含大量的類(lèi)和資源,這使得應(yīng)用程序的結(jié)構(gòu)變得混亂,難以維護(hù)。
  • 可重用性差:在多個(gè)應(yīng)用程序之間共享代碼和資源比較困難。
  • 安全性問(wèn)題:所有的類(lèi)都在同一個(gè)類(lèi)路徑中,這可能導(dǎo)致意外的訪(fǎng)問(wèn)和依賴(lài)關(guān)系。

Java模塊化解決了這些問(wèn)題。模塊是一種新的編程單元,它可以包含類(lèi)、資源和其他模塊的依賴(lài)關(guān)系。模塊化的代碼更容易維護(hù),更容易重用,同時(shí)也提供了更好的安全性。

模塊化的基本概念

  1. 模塊(Module)
    一個(gè)模塊是一個(gè)可重用的單元,它包含了一組相關(guān)的類(lèi)和資源。每個(gè)模塊都有一個(gè)名字,并可以聲明自己的依賴(lài)關(guān)系。
  2. 模塊聲明(Module Declaration)
    一個(gè)模塊聲明是一個(gè)包含在module-info.java文件中的文件,它定義了一個(gè)模塊的名稱(chēng)、依賴(lài)關(guān)系和其他特性。
  3. 模塊路徑(Module Path)
    模塊路徑是一組目錄和JAR文件,其中包含了模塊的JMOD文件和module-info.class文件。模塊路徑用于告訴JVM哪些模塊可用。
  4. 模塊化 JAR 文件(Modular JAR File)
    模塊化JAR文件是一種特殊類(lèi)型的JAR文件,它包含了一個(gè)模塊的類(lèi)和資源,以及module-info.class文件。
  5. 自動(dòng)模塊(Automatic Module)
    如果一個(gè)JAR文件沒(méi)有module-info.class文件,它被稱(chēng)為自動(dòng)模塊。自動(dòng)模塊的名稱(chēng)基于JAR文件的文件名,并且具有一些默認(rèn)的依賴(lài)關(guān)系。
  6. 依賴(lài)性(Dependency)
    一個(gè)模塊可以聲明對(duì)其他模塊的依賴(lài)關(guān)系,以便在編譯時(shí)和運(yùn)行時(shí)使用其他模塊的類(lèi)和資源。

模塊化下的類(lèi)加載器

為了保證兼容性,JDK 9并沒(méi)有從根本上動(dòng)搖從JDK 1.2以來(lái)運(yùn)行了二十年之久的三層類(lèi)加載器架構(gòu)以及雙親委派模型。但是為了模塊化系統(tǒng)的順利施行,模塊化下的類(lèi)加載器仍然發(fā)生了一些變化。

  1. 擴(kuò)展類(lèi)加載器的替代: 擴(kuò)展類(lèi)加載器(Extension Class Loader)被平臺(tái)類(lèi)加載器(Platform Class Loader)取代。這是因?yàn)镴DK 9基于模塊化構(gòu)建,整個(gè)Java類(lèi)庫(kù)已經(jīng)天然滿(mǎn)足可擴(kuò)展的需求,因此不再需要維護(hù)<JAVA_HOME>\lib\ext目錄。之前通過(guò)這個(gè)目錄來(lái)加載擴(kuò)展類(lèi)庫(kù)的擴(kuò)展類(lèi)加載器也就不再需要,完成了它的歷史使命。
  2. 取消<JAVA_HOME>\jre目錄: 在新版的JDK中取消了<JAVA_HOME>\jre目錄。這是因?yàn)楝F(xiàn)在可以根據(jù)需要組合構(gòu)建出程序運(yùn)行所需的JRE。例如,如果只需要使用java.base模塊中的類(lèi)型,可以通過(guò)jlink命令輕松地打包出一個(gè)只包含所需模塊的“JRE”。

最后,JDK 9中雖然仍然維持著三層類(lèi)加載器和雙親委派的架構(gòu),但類(lèi)加載的委派關(guān)系也發(fā)生了 變動(dòng)。

當(dāng)平臺(tái)及應(yīng)用程序類(lèi)加載器收到類(lèi)加載請(qǐng)求,在委派給父加載器加載前,要先判斷該類(lèi)是否能 夠歸屬到某一個(gè)系統(tǒng)模塊中,如果可以找到這樣的歸屬關(guān)系,就要優(yōu)先委派給負(fù)責(zé)那個(gè)模塊的加載器完成加載,也許這可以算是對(duì)雙親委派的破壞

啟動(dòng)類(lèi)加載器負(fù)責(zé)加載的模塊

平臺(tái)類(lèi)加載器負(fù)責(zé)加載的模塊

應(yīng)用程序類(lèi)加載器負(fù)責(zé)加載的模塊

總結(jié)

以上是生活随笔為你收集整理的JVM学习-类加载机制的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。