理解JVM(四):JVM類(lèi)加載機(jī)制

Class文件

我們寫(xiě)的Java代碼,經(jīng)過(guò)編譯器編譯之后,就成為了.class文件,從本地機(jī)器碼變成了字節(jié)碼。Class文件是一組以8位字節(jié)為基礎(chǔ)單位的二進(jìn)制流,各個(gè)數(shù)據(jù)項(xiàng)目嚴(yán)格按照順序緊湊地排列在Class文件之中,中間沒(méi)有添加任何分隔符,這使得整個(gè)Class文件中存儲(chǔ)的內(nèi)容幾乎全部是程序運(yùn)行的必要數(shù)據(jù),沒(méi)有空隙存在。Class文件中只有2種數(shù)據(jù)結(jié)構(gòu):無(wú)符號(hào)數(shù)和表。

每個(gè)Class文件的頭4個(gè)字節(jié)稱(chēng)為魔數(shù)(Magic Number),值為0xCAFEBABE。緊接著4個(gè)字節(jié)是Class文件的版本號(hào)。再往后,就是類(lèi)的具體信息了,比如常量池、類(lèi)索引、父類(lèi)索引、接口索引、字段、方法等信息了。

所謂類(lèi)的加載,就是把Class文件讀到內(nèi)存中。

類(lèi)的生命周期

image

類(lèi)從被加載到虛擬機(jī)內(nèi)存中開(kāi)始,到卸載出內(nèi)存為止,它的整個(gè)生命周期包括:加載(Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個(gè)階段。其中驗(yàn)證、準(zhǔn)備、解析3個(gè)部分統(tǒng)稱(chēng)為連接(Linking)。

加載、驗(yàn)證、準(zhǔn)備、初始化和卸載這5個(gè)階段的順序是確定的,類(lèi)的加載過(guò)程必須按照這種順序按部就班地開(kāi)始,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開(kāi)始,這是為了支持Java語(yǔ)言的運(yùn)行時(shí)綁定(也稱(chēng)為動(dòng)態(tài)綁定或晚期綁定)。注意,是按部就班地“開(kāi)始”,而不是按部就班地“進(jìn)行”或“完成”,強(qiáng)調(diào)這點(diǎn)是因?yàn)檫@些階段通常都是互相交叉地混合式進(jìn)行的,通常會(huì)在一個(gè)階段執(zhí)行的過(guò)程中調(diào)用、激活另外一個(gè)階段。

加載

在加載階段,虛擬機(jī)做3件事:

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

驗(yàn)證

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

驗(yàn)證階段大致上會(huì)完成4個(gè)階段的檢驗(yàn)動(dòng)作

  1. 文件格式驗(yàn)證:驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理。比如是否以魔數(shù)0xCAFEBABE開(kāi)頭,主、次版本號(hào)是否能被當(dāng)前虛擬機(jī)處理,常量類(lèi)型,指向常量的索引是否符合要求等。這階段的驗(yàn)證是基于二進(jìn)制字節(jié)流進(jìn)行的,只有通過(guò)了這個(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é)流。
  2. 元數(shù)據(jù)驗(yàn)證:對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,以保證其描述的信息符合Java語(yǔ)言規(guī)范的要求。比如繼承關(guān)系。
  3. 字節(jié)碼驗(yàn)證:對(duì)類(lèi)的方法體進(jìn)行校驗(yàn)分析,保證被校驗(yàn)類(lèi)的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的事件。通過(guò)數(shù)據(jù)流和控制流分析,確定程序語(yǔ)義是合法的、符合邏輯的。
  4. 符號(hào)引用驗(yàn)證:對(duì)類(lèi)自身以外(常量池中的各種符號(hào)引用)的信息進(jìn)行匹配性校驗(yàn),確保解析動(dòng)作能正常執(zhí)行。它發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候,這個(gè)轉(zhuǎn)化動(dòng)作將在連接的第三階段——解析階段中發(fā)生。

驗(yàn)證階段是非常重要的,但不是必須的。它對(duì)程序運(yùn)行期沒(méi)有影響,如果所引用的類(lèi)經(jīng)過(guò)反復(fù)驗(yàn)證,那么可以考慮采用-Xverify:none參數(shù)來(lái)關(guān)閉大部分的類(lèi)驗(yàn)證措施,以縮短虛擬機(jī)類(lèi)加載的時(shí)間。

準(zhǔn)備

準(zhǔn)備階段是正式為類(lèi)變量分配內(nèi)存并設(shè)置類(lèi)變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。這個(gè)階段中有兩個(gè)容易產(chǎn)生混淆的概念需要強(qiáng)調(diào)一下,首先,這時(shí)候進(jìn)行內(nèi)存分配的僅包括類(lèi)變量(被static修飾的變量),而不包括實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在Java堆中。其次,這里所說(shuō)的初始值“通常情況”下是數(shù)據(jù)類(lèi)型的零值。

假設(shè)一個(gè)類(lèi)變量的定義為:public static int value = 123;

那變量value在準(zhǔn)備階段過(guò)后的初始值為0而不是123,因?yàn)檫@時(shí)候尚未開(kāi)始執(zhí)行任何Java
方法,而把value賦值為123的putstatic指令是程序被編譯后,存放于類(lèi)構(gòu)造器<clinit>()方
法之中,所以把value賦值為123的動(dòng)作將在初始化階段才會(huì)執(zhí)行。

當(dāng)然也有特殊情況:如果類(lèi)字段的字段屬性表中存在ConstantValue屬性,那在準(zhǔn)備階段變量value就會(huì)被初始化為ConstantValue屬性所指定的值。

假設(shè)上面類(lèi)變量value的定義變?yōu)椋簆ublic static final int value = 123;

編譯時(shí)Javac將會(huì)為value生成ConstantValue屬性,在準(zhǔn)備階段虛擬機(jī)就會(huì)根據(jù)ConstantValue的設(shè)置將value賦值為123。

解析

解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程。解析動(dòng)作主要針對(duì)類(lèi)或接口、字段、類(lèi)方法、接口方法、方法類(lèi)型、方法句柄和調(diào)用點(diǎn)限定符7類(lèi)符號(hào)引用進(jìn)行。

  • 符號(hào)引用(Symbolic References):符號(hào)引用以一組符號(hào)來(lái)描述所引用的目標(biāo),符號(hào)可
    以是任何形式的字面量,只要使用時(shí)能無(wú)歧義地定位到目標(biāo)即可。符號(hào)引用與虛擬機(jī)實(shí)現(xiàn)的
    內(nèi)存布局無(wú)關(guān),引用的目標(biāo)并不一定已經(jīng)加載到內(nèi)存中。各種虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局可以各
    不相同,但是它們能接受的符號(hào)引用必須都是一致的,因?yàn)榉?hào)引用的字面量形式明確定義
    在Java虛擬機(jī)規(guī)范的Class文件格式中。
  • 直接引用(Direct References):直接引用可以是直接指向目標(biāo)的指針、相對(duì)偏移量或是
    一個(gè)能間接定位到目標(biāo)的句柄。直接引用是和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān)的,同一個(gè)符號(hào)引
    用在不同虛擬機(jī)實(shí)例上翻譯出來(lái)的直接引用一般不會(huì)相同。如果有了直接引用,那引用的目
    標(biāo)必定已經(jīng)在內(nèi)存中存在。

初始化

這一步開(kāi)始執(zhí)行類(lèi)中定義的Java程序代碼(或者說(shuō)是字節(jié)碼)。虛擬機(jī)會(huì)保證一個(gè)類(lèi)的初始化方法在多線(xiàn)程環(huán)境中被正確地加鎖、同步,如果多個(gè)線(xiàn)程同時(shí)去初始化一個(gè)類(lèi),那么只會(huì)有一個(gè)線(xiàn)程去執(zhí)行這個(gè)類(lèi)的初始化方法,其他線(xiàn)程都需要阻塞等待,直到活動(dòng)線(xiàn)程執(zhí)行完畢。

JVM初始化步驟

  1. 假如這個(gè)類(lèi)還沒(méi)有被加載和連接,則程序先加載并連接該類(lèi)
  2. 假如該類(lèi)的直接父類(lèi)還沒(méi)有被初始化,則先初始化其直接父類(lèi)
  3. 假如類(lèi)中有初始化語(yǔ)句,則系統(tǒng)依次執(zhí)行這些初始化語(yǔ)句

類(lèi)初始化時(shí)機(jī)

只有當(dāng)主動(dòng)使用一個(gè)類(lèi)的時(shí)候才會(huì)觸發(fā)這個(gè)類(lèi)的初始化,類(lèi)的主動(dòng)使用包括以下六種:

  • 創(chuàng)建類(lèi)的實(shí)例,也就是new的方式
  • 訪(fǎng)問(wèn)某個(gè)類(lèi)或接口的靜態(tài)變量,或者對(duì)該靜態(tài)變量賦值
  • 調(diào)用類(lèi)的靜態(tài)方法
  • 反射,比如Class.forName(“com.mysql.jdbc.Driver”)
  • 初始化某個(gè)類(lèi)的子類(lèi),則其父類(lèi)也會(huì)被初始化
  • Java虛擬機(jī)啟動(dòng)時(shí)被標(biāo)明為啟動(dòng)類(lèi)的類(lèi)(Java Test),直接使用java.exe命令來(lái)運(yùn)行某個(gè)主類(lèi)

類(lèi)加載器

虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)把類(lèi)加載階段中的“通過(guò)一個(gè)類(lèi)的全限定名來(lái)獲取描述此類(lèi)的二進(jìn)制字節(jié)流”這個(gè)動(dòng)作放到Java虛擬機(jī)外部去實(shí)現(xiàn),以便讓?xiě)?yīng)用程序自己決定如何去獲取所需要的類(lèi)。實(shí)現(xiàn)這個(gè)動(dòng)作的代碼模塊稱(chēng)為“類(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)加載器可以劃分為以下3種:

  1. 啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader):負(fù)責(zé)加載存放在JAVA_HOME\lib目錄中的,或被-Xbootclasspath參數(shù)指定的路徑中的,并且能被虛擬機(jī)識(shí)別的類(lèi)庫(kù)(如rt.jar,所有的java.開(kāi)頭的類(lèi)均被Bootstrap ClassLoader加載)。啟動(dòng)類(lèi)加載器是無(wú)法被Java程序直接引用的。
  2. 擴(kuò)展類(lèi)加載器(Extension ClassLoader):這個(gè)加載器由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn),它負(fù)責(zé)加載JAVA_HOME\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類(lèi)庫(kù),開(kāi)發(fā)者可以直接使用擴(kuò)展類(lèi)加載器。
  3. 應(yīng)用程序類(lèi)加載器(Application ClassLoader):該類(lèi)加載器由sun.misc.Launcher$AppClassLoader來(lái)實(shí)現(xiàn),它負(fù)責(zé)加載用戶(hù)類(lèi)路徑(ClassPath)所指定的類(lèi),開(kāi)發(fā)者可以直接使用該類(lèi)加載器,如果應(yīng)用程序中沒(méi)有自定義過(guò)自己的類(lèi)加載器,一般情況下這個(gè)就是程序中默認(rèn)的類(lèi)加載器。

我們的應(yīng)用程序都是由這3種類(lèi)加載器互相配合進(jìn)行加載的,如果有必要,還可以加入
自己定義的類(lèi)加載器。

image

雙親委派模型要求除了頂層的啟動(dòng)類(lèi)加載器外,其余的類(lèi)加載器都應(yīng)當(dāng)有自己的父類(lèi)加載器。這里類(lèi)加載器之間的父子關(guān)系一般不會(huì)以繼承的關(guān)系來(lái)實(shí)現(xiàn),而是都使用組合關(guān)系來(lái)復(fù)用父加載器的代碼。它不是強(qiáng)制性的約束模型,而是Java設(shè)計(jì)者推薦的一種類(lèi)加載器實(shí)現(xiàn)方式。

雙親委派模型的工作過(guò)程:如果一個(gè)類(lèi)加載器收到了類(lèi)加載的請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類(lèi),而是把這個(gè)請(qǐng)求委派給父類(lèi)加載器去完成,每一個(gè)層次的類(lèi)加載器都是如此,因此所有的加載請(qǐng)求最終都應(yīng)該傳送到頂層的啟動(dòng)類(lèi)加載器中,只有當(dāng)父加載器反饋?zhàn)约簾o(wú)法完成這個(gè)加載請(qǐng)求(它的搜索范圍中沒(méi)有找到所需的類(lèi))時(shí),子加載器才會(huì)嘗試自己去加載。

ClassLoader源碼分析:

public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
}

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // 首先判斷該類(lèi)型是否已經(jīng)被加載
        Class c = findLoadedClass(name);
        if (c == null) {
            //如果沒(méi)有被加載,就委托給父類(lèi)加載或者委派給啟動(dòng)類(lèi)加載器加載
            try {
                if (parent != null) {
                     //如果存在父類(lèi)加載器,就委派給父類(lèi)加載器加載
                    c = parent.loadClass(name, false);
                } else {
                    //如果不存在父類(lèi)加載器,就檢查是否是由啟動(dòng)類(lèi)加載器加載的類(lèi),通過(guò)調(diào)用本地方法native Class findBootstrapClass(String name)
                    c = findBootstrapClass0(name);
                }
            } catch (ClassNotFoundException e) {
                // 如果父類(lèi)加載器和啟動(dòng)類(lèi)加載器都不能完成加載任務(wù),才調(diào)用自身的加載功能
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }

通過(guò)分析源碼,我們知道,雙親委派模型可以保證每個(gè)類(lèi)都只會(huì)被加載一次(類(lèi)似緩存機(jī)制)。

參考

?著作權(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)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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