類(lèi)文件的結(jié)構(gòu)、JVM 的類(lèi)加載過(guò)程、類(lèi)加載機(jī)制、類(lèi)加載器、雙親委派模型

一、類(lèi)文件的結(jié)構(gòu)

我們都知道,各種不同平臺(tái)的虛擬機(jī),都支持 “字節(jié)碼 Byte Code” 這種程序存儲(chǔ)格式,這構(gòu)成了 Java 平臺(tái)無(wú)關(guān)性的基石。甚至現(xiàn)在平臺(tái)無(wú)關(guān)性也開(kāi)始演變出 “語(yǔ)言無(wú)關(guān)性” ,就是其他語(yǔ)言也可以運(yùn)行在 Java 虛擬機(jī)之上,比如現(xiàn)在的 Kotlin、Scala 等。

實(shí)現(xiàn)語(yǔ)言無(wú)關(guān)性的基礎(chǔ)仍然是虛擬機(jī)和字節(jié)碼存儲(chǔ)格式,Java 虛擬機(jī)<typo id="typo-181" data-origin="步" ignoretag="true">步</typo>包括 Java 語(yǔ)言在內(nèi)的任何語(yǔ)言綁定,他只和 “Class 文件” 這種特定的二進(jìn)制文件格式所關(guān)聯(lián),Class 文件中包含了 Java 虛擬機(jī)指令集、符號(hào)表以及其他若干輔助信息。

Java 的各種語(yǔ)法、關(guān)鍵字、常量變量和運(yùn)算符號(hào)的語(yǔ)義最終都會(huì)由多條字節(jié)碼指令組合來(lái)表達(dá),這決定了字節(jié)碼指令所能提供的語(yǔ)言描述能力必須比 Java 語(yǔ)言本身更強(qiáng)大才行。

jvm 提供的語(yǔ)言無(wú)關(guān)性如下圖所示:

Java 技術(shù)能夠一直保持著非常良好的向后兼容性,Class文件結(jié)構(gòu)的穩(wěn)定功不可沒(méi)。JDK1.2 時(shí)代的 Java 虛擬機(jī)中就定義好的 Class 文件格式的各項(xiàng)細(xì)節(jié),到今天幾乎沒(méi)有出現(xiàn)任何改變。Class文件格式進(jìn)行了幾次更新,但基本上只是在原有結(jié)構(gòu)基礎(chǔ)上新增內(nèi)容、擴(kuò)充功能。

類(lèi)文件格式

Class 文件格式采用一種類(lèi)似 C 語(yǔ)言結(jié)構(gòu)體的偽結(jié)構(gòu)來(lái)存儲(chǔ)數(shù)據(jù),這種偽結(jié)構(gòu)中只有兩種數(shù)據(jù)類(lèi)型:“無(wú)符號(hào)數(shù)”和“表”。后面的解析都要以這兩種數(shù)據(jù)類(lèi)型為基礎(chǔ)。

  • 無(wú)符號(hào)數(shù)屬于基本數(shù)據(jù)類(lèi)型,以 u1、u2、u4、u8分別代表1、2、4、8個(gè)字節(jié)的無(wú)符號(hào)數(shù),無(wú)符號(hào)數(shù)可以描述數(shù)字、索引引用、數(shù)量值或者utf-8 編碼構(gòu)成字符串值。
  • 表是多個(gè)無(wú)符號(hào)數(shù)或者其他表組成的復(fù)合類(lèi)型,為了便于區(qū)分,所有表的命名都是習(xí)慣性地以 “_info” 結(jié)尾。整個(gè) Class 文件本質(zhì)上也可以視為一張表。

Class 文件格式的數(shù)據(jù)項(xiàng)如下所示:

這里面可以看到,一個(gè) Class 文件的有些數(shù)據(jù)項(xiàng)是固定的 數(shù)量 × 長(zhǎng)度,有些則不是。如果一個(gè)類(lèi)型的數(shù)據(jù)數(shù)量不定,會(huì)采用多一個(gè)數(shù)據(jù)項(xiàng)來(lái)實(shí)現(xiàn),一個(gè)前置的數(shù)據(jù)項(xiàng)作為容量計(jì)數(shù)器,后面連續(xù)的數(shù)據(jù)項(xiàng),而數(shù)量就是前面的容量計(jì)數(shù)器的值,這時(shí)候這一系列連續(xù)的某一類(lèi)型的數(shù)據(jù)稱(chēng)為某一類(lèi)型的 “集合”。

比如上面的這個(gè),從字面意思也看得出來(lái),因?yàn)槌A砍乇旧砭褪呛芏喑A繌?fù)合組成的,數(shù)量就會(huì)先用一個(gè) u2 類(lèi)型的數(shù)據(jù)項(xiàng)來(lái)表示,也就是我們剛說(shuō)過(guò)的容量計(jì)數(shù)器,然后接著這個(gè)常量池集合本身就有了數(shù)量。

這么嚴(yán)格要求的原因是,Class 文件沒(méi)有任何分隔符,所以整個(gè) Class 文件的格式,順序、數(shù)量這樣的細(xì)節(jié),都是嚴(yán)格限定的,全都不允許改變。

接下來(lái),我們來(lái)看各個(gè)數(shù)據(jù)項(xiàng)的含義??偣卜譃?7 項(xiàng),按照上面的那張圖的顏色框劃分也很容易看出來(lái),并且表示的信息也是見(jiàn)名知意的。

1.1 魔數(shù)與 Class 文件的版本

通常常量池是占用 Class 文件空間最大的數(shù)據(jù)項(xiàng)之一。

Class 文件的魔數(shù)是 0xCAFEBABE,咖啡寶貝。是因?yàn)?java 開(kāi)發(fā)小組最初的關(guān)鍵成員覺(jué)得他象征著名咖啡品牌最受歡迎的咖啡,似乎對(duì) java 的商標(biāo)也有預(yù)示

  • 魔數(shù)后面的 4 個(gè)字節(jié)是 Class 文件的版本號(hào):5、6 字節(jié)是次版本號(hào),7、8字節(jié)是主版本號(hào)。

1.2 常量池

通常常量池是占用 Class 文件空間最大的數(shù)據(jù)項(xiàng)之一。

分為兩個(gè)部分,一個(gè)2字節(jié)的數(shù)據(jù)代表常量池容量計(jì)數(shù)值;下面是常量池的內(nèi)容,可以看到這里使用容量的大小用的是 constan_pool_count-1 ,因?yàn)槌A砍氐娜萘坑?jì)數(shù)是 1 開(kāi)始,而不是 0,比如這個(gè) constan_pool_count 值翻譯成十進(jìn)制是 22,那么代表常量池有 21 項(xiàng)常量。

除了常量池,剩下的數(shù)據(jù)項(xiàng)表示都是從 0 開(kāi)始計(jì)數(shù)的。

常量池中存放兩大類(lèi)常量:字面量和符號(hào)引用,具體含義和分類(lèi)很復(fù)雜,這里不介紹了。

1.3 訪問(wèn)標(biāo)志

2 個(gè)字節(jié),用于識(shí)別一些類(lèi)或者接口層次的訪問(wèn)信息,包括 “這個(gè) Class 是類(lèi)還是接口” ,“是否定義為 public 類(lèi)型”;“是否定義為 abstract 類(lèi)型”,“如果是的話,是否被聲明為final”。

2 個(gè)字節(jié)總共有 16 個(gè)標(biāo)志位,目前只定義了 9 個(gè),沒(méi)有使用的標(biāo)志位一律置為 0。

1.4 類(lèi)索引、父類(lèi)索引和接口索引集合

Class 文件中由這三項(xiàng)數(shù)據(jù)來(lái)確定該類(lèi)的繼承關(guān)系,顯然因?yàn)?java 是單繼承,卻可以實(shí)現(xiàn)多個(gè)接口,所以有了 super_class 是一個(gè) u2 的數(shù)據(jù),而 interfaces 則需要一個(gè) interfaces_count 。

類(lèi)索引+父類(lèi)索引這兩項(xiàng)的值,就指向的是一個(gè) 類(lèi)描述符常量,通過(guò)這個(gè)索引值就能找到對(duì)應(yīng)的類(lèi)。

1.5 字段表集合

1.7 屬性表集合

  1. 類(lèi)級(jí)變量;
  2. 實(shí)例級(jí)變量。

但是不包括在方法內(nèi)部聲明的局部變量。

因?yàn)?field_info 本身也是一個(gè)表,具體的這里就不說(shuō)明。

1.6 方法表集合

和字段表集合類(lèi)似。

但是放發(fā)表的結(jié)構(gòu)有一個(gè)特點(diǎn),就是里面并沒(méi)有方法體里的代碼,方法體的代碼在下一個(gè)屬性表里。

1.7 屬性表集合

Class 文件,字段表,方法表,三個(gè)集合內(nèi)部都可以嵌套攜帶屬性表集合。

具體屬性表的格式之類(lèi)的,也是很復(fù)雜,這里不贅述。

1.8 字節(jié)碼指令

Java 虛擬機(jī)的指令由一個(gè)字節(jié)長(zhǎng)度的、代表著某種特定操作含義的數(shù)字以及跟隨其后的零至多個(gè)代表此操作所需的參數(shù)構(gòu)成。

由于Java虛擬機(jī)采用面向操作數(shù)棧而不是面向寄存器的架構(gòu),所以大多數(shù)指令都不包含操作數(shù),只有一個(gè)操作碼,指令參數(shù)都存放在操作數(shù)棧中。

在Java虛擬機(jī)的指令集中,大多數(shù)指令都包含其操作所對(duì)應(yīng)的數(shù)據(jù)類(lèi)型信息。

舉個(gè)例子,iload指令用于從局部變量表中加載 int 型的數(shù)據(jù)到操作數(shù)棧中,而 fload 指令加載的則是 float 類(lèi)型的數(shù)據(jù)。這兩條指令的操作在虛擬機(jī)內(nèi)部可能會(huì)是由同一段代碼來(lái)實(shí)現(xiàn)的,但在 Class文件中它們必須擁有各自獨(dú)立的操作碼。

對(duì)于大部分與數(shù)據(jù)類(lèi)型相關(guān)的字節(jié)碼指令,它們的操作碼助記符中都有特殊的字符來(lái)表明專(zhuān)門(mén)為哪種數(shù)據(jù)類(lèi)型服務(wù):i <typo id="typo-2577" data-origin="代表對(duì)" ignoretag="true">代表對(duì)</typo> int 類(lèi)型的數(shù)據(jù)操作, l 代表 long,s 代表 short,b 代表 byte,c 代表 char,f 代表 float,d 代表 double,a 代表 reference。

字節(jié)碼指令可以分為:

  • 加載和存儲(chǔ)指令:講數(shù)據(jù)在棧幀中的局部變量表和操作數(shù)棧之間來(lái)回傳輸;
  • 運(yùn)算指令:對(duì)兩個(gè)操作數(shù)棧上的值進(jìn)行運(yùn)算,并把結(jié)果重新存入操作數(shù)棧頂;
  • 類(lèi)型轉(zhuǎn)換指令:將不同數(shù)值類(lèi)型相互轉(zhuǎn)換;
  • 對(duì)象創(chuàng)建與訪問(wèn)指令;
  • 操作數(shù)棧管理指令:直接操作操作數(shù)棧的指令,出棧入棧等;
  • 控制轉(zhuǎn)移指令:讓jvm從指定位置的下一條指令繼續(xù)執(zhí)行程序,可以認(rèn)為是在修改PC寄存器的值;
  • 方法調(diào)用和返回指令;
  • 異常處理指令;
  • 同步指令:Java虛擬機(jī)可以支持方法級(jí)的同步方法內(nèi)部一段指令序列的同步,這兩種同步結(jié)構(gòu)都是使用管程( Monitor,更常見(jiàn)的是直接將它稱(chēng)為“鎖”) 來(lái)實(shí)現(xiàn)的。方法級(jí)的同步是隱式的,無(wú)須通過(guò)字節(jié)碼指令來(lái)控制,它實(shí)現(xiàn)在方法調(diào)用和返回操作之中。虛擬機(jī)可以從方法常量池中的方法表結(jié)構(gòu)中的 ACC_SYNCHRONIZED 訪問(wèn)標(biāo)志得知一個(gè)方法是否被聲明為同步方法。當(dāng)方法調(diào)用時(shí),調(diào)用指令將會(huì)檢查方法的 ACC_SYNCHRONIZED 訪問(wèn)標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程就要求先成功持有管程,然后才能執(zhí)行方法,最后當(dāng)方法完成 (無(wú)論是正常完成還是非正常完成)時(shí)釋放管程。在方法執(zhí)行期間,執(zhí)行線程持有了管程,其他任何線程都無(wú)法再獲取到同一個(gè)管程。如果一個(gè)同步方法執(zhí)行期間拋出了異常,并且在方法內(nèi)部無(wú)法處理此異常,那這個(gè)同步方法所持有的管程將在異常拋到同步方法邊界之外時(shí)自動(dòng)釋放。同步一段指令集序列通常是由 Java 語(yǔ)言中的 synchronized 語(yǔ)句塊來(lái)表示的,Java虛擬機(jī)的指令集中有 monitor enter 和 monitor exit 兩條指令來(lái)支持 synchronized 關(guān)鍵字的語(yǔ)義。正確實(shí)現(xiàn) synchronized 關(guān)鍵字需要 Javac 編譯器與 Java 虛擬機(jī)兩者共同協(xié)作支持。

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

定義:

Java 虛擬機(jī)把描述類(lèi)的數(shù)據(jù)從 Class 文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的 Java 類(lèi)型,這個(gè)過(guò)程被稱(chēng)作虛擬機(jī)的類(lèi)加載機(jī)制。

從定義里就可以看出來(lái),java 和哪些編譯時(shí)要進(jìn)行連接的語(yǔ)言不同,java 的類(lèi)型的加載、連接、初始化都是程序運(yùn)行期間完成的,這給 java 應(yīng)用提供了極高的擴(kuò)展性,java 的可動(dòng)態(tài)擴(kuò)展的語(yǔ)言特性就是依賴于運(yùn)行期動(dòng)態(tài)加載和動(dòng)態(tài)連接這個(gè)特點(diǎn)實(shí)現(xiàn)的

例如,編寫(xiě)一個(gè)面向接口的程序,可以等到運(yùn)行時(shí)再指定其實(shí)際的實(shí)現(xiàn)類(lèi),用戶可以通過(guò) java 預(yù)置或自定義類(lèi)加載器,讓某個(gè)本地應(yīng)用程序在運(yùn)行時(shí)從網(wǎng)絡(luò)或者其他地方加載一個(gè)二進(jìn)制流作為其程序代碼的一部分。

(后面說(shuō)的類(lèi)加載的“類(lèi)”,實(shí)際上可能是接口或者類(lèi))

2.1 一個(gè)類(lèi)的生命周期

如上圖所示,一個(gè)類(lèi)從被加載到虛擬機(jī)的內(nèi)存中開(kāi)始,到卸載出內(nèi)存為止,生命周期分為 7 個(gè)階段:

  1. 加載;
  2. 連接:驗(yàn)證;準(zhǔn)備;解析;
  3. 初始化;
  4. 使用;
  5. 卸載。

其中驗(yàn)證、準(zhǔn)備、解析三個(gè)階段可以合起來(lái)稱(chēng)為連接。

加載、驗(yàn)證、準(zhǔn)備、初始化和卸載這五個(gè)階段的順序是確定的,類(lèi)型的加載過(guò)程必須按照這種順序按部就班地開(kāi)始,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開(kāi)始,這是為了支持Java語(yǔ)言的運(yùn)行時(shí)綁定特性(也稱(chēng)為動(dòng)態(tài)綁定或晚期綁定)。

請(qǐng)注意“開(kāi)始”,而不是按部就班地“進(jìn)行“,或按部就班地“完成”,強(qiáng)調(diào)這點(diǎn)是因?yàn)檫@些階段通常都是互相交叉地混合進(jìn)行的,會(huì)在一個(gè)階段執(zhí)行的過(guò)程中調(diào)用、激活另一個(gè)階段。

2.2 什么時(shí)候類(lèi)會(huì)被加載

關(guān)于什么時(shí)候需要開(kāi)始類(lèi)加載過(guò)程的第一個(gè)階段“加載”,虛擬機(jī)規(guī)范沒(méi)有強(qiáng)制約束,可以交給虛擬機(jī)的具體實(shí)現(xiàn)。但是初始化階段,嚴(yán)格規(guī)定了有且只有 6 種情況必須立即對(duì)類(lèi)進(jìn)行初始化(這就意味著,加載驗(yàn)證準(zhǔn)備都必須在此之前開(kāi)始):

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

上面的六種場(chǎng)景中的行為,叫做對(duì)一個(gè)類(lèi)型進(jìn)行主動(dòng)引用。除了這六種外的引用類(lèi)型的方式都不會(huì)觸發(fā)初始化,被稱(chēng)為被動(dòng)引用。

2.3 類(lèi)加載的過(guò)程

接下來(lái)看詳細(xì)過(guò)程。

2.3.1 加載

注意啊,“加載”只是整個(gè)“類(lèi)加載”中的一個(gè)階段。

加載階段,虛擬機(jī)主要做三件事:

  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ù)的訪問(wèn)入口。

其中,第一點(diǎn)的來(lái)源可以是各種各樣,zip包,網(wǎng)絡(luò)中,也可以利用動(dòng)態(tài)代理技術(shù)在運(yùn)行時(shí)計(jì)算生成。

第二點(diǎn)是要用到類(lèi)加載器的,相對(duì)于五個(gè)階段的其他階段:

  1. 非數(shù)組類(lèi)型的加載階段(就是當(dāng)前階段)是可控性最強(qiáng)的,可以通過(guò) jvm 內(nèi)置的引導(dǎo)類(lèi)加載器,也可以自定義類(lèi)加載器,開(kāi)發(fā)人員通過(guò)定義自己的類(lèi)加載器去控制字節(jié)流的獲取方式(重寫(xiě)一個(gè)類(lèi)加載器的 findClass() 或 loadClass() 方法)。
  2. 數(shù)組類(lèi)型來(lái)說(shuō),數(shù)組類(lèi)不通過(guò)類(lèi)加載器創(chuàng)建,而是由 jvm 直接在內(nèi)存中動(dòng)態(tài)構(gòu)造出來(lái),但是數(shù)組的元素類(lèi)本身最終還是要靠類(lèi)加載器來(lái)完成加載,這個(gè)過(guò)程就是 jvm 把數(shù)組降一個(gè)維度,然后決定繼續(xù)遞歸還是直接可以和類(lèi)加載器關(guān)聯(lián)。

第三點(diǎn)就是如上面所說(shuō)。

2.3.2 驗(yàn)證(連接之 1 )

驗(yàn)證是連接階段的第一步,這一階段的目的是確保 Class 文件的字節(jié)流中包含的信息符合《Java虛擬機(jī)規(guī)范》的全部約束要求,保證這些信息被當(dāng)作代碼運(yùn)行后不會(huì)危害虛擬機(jī)自身的安全。

為什么要驗(yàn)證?

結(jié)合上一個(gè)步驟,就是因?yàn)?Class 文件不一定就是 java 源碼編譯來(lái)的,可能是各種途徑,甚至是自己手敲的 01 碼,所以有必要驗(yàn)證字節(jié)碼。

一般驗(yàn)證的內(nèi)容分為四個(gè):

  1. 文件格式驗(yàn)證:檢查字節(jié)流是否符合 Class 文件格式的規(guī)范,就是魔數(shù)啊、版本號(hào)之類(lèi)的;
  2. 元數(shù)據(jù)驗(yàn)證:上一步格式?jīng)]問(wèn)題,然后對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,看類(lèi)關(guān)系、字段方法有沒(méi)有矛盾之類(lèi);
  3. 字節(jié)碼驗(yàn)證:最復(fù)雜的一步,通過(guò)數(shù)據(jù)流分析和控制流分析,確定程序語(yǔ)義合法、合邏輯,上一步數(shù)據(jù)類(lèi)型等到?jīng)]問(wèn)題,這一步就要進(jìn)入方法體的邏輯分析;
  4. 符號(hào)引用驗(yàn)證:發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候,轉(zhuǎn)化動(dòng)作本身實(shí)在連接之3階段——解析階段發(fā)生的。(所以前面說(shuō)這些順序只是開(kāi)始順序,執(zhí)行的時(shí)候是互相切換的),目的是驗(yàn)證引用的類(lèi)、字段等內(nèi)容是否能找到并訪問(wèn)之類(lèi)的。

驗(yàn)證階段很重要,卻不一定必須執(zhí)行,因?yàn)橥ㄟ^(guò)了驗(yàn)證階段,后面對(duì)程序執(zhí)行就沒(méi)有影響了,如果程序反復(fù)被驗(yàn)證和使用過(guò)就可以用參數(shù)關(guān)閉大部分的類(lèi)驗(yàn)證措施:

-Xverify: none

2.3.3 準(zhǔn)備(連接之 2 )

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

從概念上講,這些變量所使用的內(nèi)存都應(yīng)當(dāng)在方法區(qū)中進(jìn)行分配,但方法區(qū)本身是一個(gè)邏輯上的區(qū)域。

在上一篇,jvm 的內(nèi)存結(jié)構(gòu)里多次強(qiáng)調(diào)。在 JDK7及之前, HotSpot 使用永久代來(lái)實(shí)現(xiàn)方法區(qū),所以還可以勉強(qiáng)把方法區(qū)這個(gè)概念保留;而在 JDK8 及之后,永久代也沒(méi)有了,所以類(lèi)變量隨著 Class 對(duì)象一起存放在 Java 堆中,這時(shí)候 “類(lèi)變量在方法區(qū)” 就有點(diǎn)牽強(qiáng)。

注意:

  1. 這個(gè)階段進(jìn)行內(nèi)存分配的僅包括類(lèi)變量,不包括實(shí)例變量。現(xiàn)在講的整個(gè)過(guò)程都只是類(lèi)加載的過(guò)程,實(shí)例變量會(huì)在對(duì)象實(shí)例化的時(shí)候隨對(duì)象一起分配在 java 堆中。
  2. 設(shè)置初始值通常指的是數(shù)據(jù)類(lèi)型的 0 值。

比如:

public static int value = 123;

經(jīng)過(guò)這里的準(zhǔn)備階段,初始值 value 是 0,因?yàn)檫@個(gè)時(shí)候任何 java 方法都沒(méi)有執(zhí)行,初始化的指令是 putstatic ,這個(gè)指令是在類(lèi)構(gòu)造器的 <clinit>() 方法里的。

所以 value 變成 123 是在類(lèi)的初始化階段才會(huì)執(zhí)行的,就是 2.3.5 。

2.3.4 解析(連接之 3 )

解析階段是 Java 虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程。

前面的 Class 文件格式部分提過(guò)一次,那解析階段中所說(shuō)的直接引用與符號(hào)引用又有什么關(guān)聯(lián)呢?

  • 符號(hào)引用(Symbolic References):一組符號(hào)來(lái)描述引用目標(biāo),任何字面量,只要能定位就行。
  • 直接引用(Direct References):直接指向目標(biāo)的指針、相對(duì)偏移量或者一個(gè)間接定位到目標(biāo)的句柄。

解析動(dòng)作主要針對(duì) 7 類(lèi)符號(hào)引用進(jìn)行轉(zhuǎn)換:

  1. 類(lèi)或接口;
  2. 字段;
  3. 類(lèi)方法;
  4. 接口方法;
  5. 方法類(lèi)型;
  6. 方法句柄;
  7. 調(diào)用點(diǎn)限定符。

2.3.5 初始化

類(lèi)的初始化階段是類(lèi)加載過(guò)程的最后一個(gè)步驟。

之前介紹的幾個(gè)類(lèi)加載的動(dòng)作里,除了在加載階段用戶應(yīng)用程序可以通過(guò)自定義類(lèi)加載器的方式局部參與外,其余動(dòng)作都完全由Java虛擬機(jī)來(lái)主導(dǎo)控制。直到初始化階段,Java 虛擬機(jī)才真正開(kāi)始執(zhí)行類(lèi)中編寫(xiě)的Java程序代碼,將主導(dǎo)權(quán)移交給應(yīng)用程序。

2.3.3 的準(zhǔn)備階段,已經(jīng)給變量賦過(guò)值了,是初始 0 值,而初始化階段,會(huì)根據(jù)代碼初始化類(lèi)變量和其他資源,另一種更直接的形式來(lái)表達(dá)這個(gè)過(guò)程:

初始化階段就是執(zhí)行類(lèi)構(gòu)造器的 <clinit>() 方法的過(guò)程,這個(gè)方法不是程序員自己寫(xiě)的,是 javac 編譯器的自動(dòng)生成物。

2.4 類(lèi)加載器

Java虛擬機(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)。(就是上面講的類(lèi)加載過(guò)程的第一個(gè)步驟)

實(shí)現(xiàn)這個(gè)動(dòng)作的代碼被稱(chēng)為 “類(lèi)加載器” ( Class loader)。

2.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)空間。

這句話可以表達(dá)得更通俗一些:比較兩個(gè)類(lèi)是否“相等”,只有在這兩個(gè)類(lèi)是由同一個(gè)類(lèi)加載器加載的前提下才有意義,否則,即使這兩個(gè)類(lèi)來(lái)源于同一個(gè)Class文件,被同一個(gè)Java虛擬機(jī)加載,只要加載它們的類(lèi)加載器不同,那這兩個(gè)類(lèi)就必定不相等。

這里的相等,包括比較對(duì)象的 equals() 方法,isInstance() 方法、isAssignableFrom() 等的返回結(jié)果,以及 instanceof 關(guān)鍵字的判定結(jié)果。

2.4.2 雙親委派模型

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

  • 一種是啟動(dòng)類(lèi)加載器( BootstrapClassloader),這個(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ì)致一些。自 JDK12 以來(lái),Java 一直保著三層類(lèi)加載器、雙親委派的類(lèi)加載架構(gòu)。

2.4.2.1 三層類(lèi)加載器

注意:下面提及的源碼目錄在JDK9之后,因?yàn)槟K化的改變,所以按照這些目錄大概率自己的 jdk 文件里找不到的。

  1. 啟動(dòng)類(lèi)加載器 ( Bootstrap Class Loader)

前面已經(jīng)介紹過(guò),這個(gè)類(lèi)加載器負(fù)責(zé)加載存放在 <JAVA_HOME>\lib 目錄,或者被 -Xbootclasspath 參數(shù)所指定的路徑中存放的,而且是 Java 虛擬機(jī)能夠識(shí)別的(按照文件名) 類(lèi)庫(kù)加載到虛擬機(jī)的內(nèi)存中。

啟動(dòng)類(lèi)加載器無(wú)法被 Java 程序直接引用,用戶在編寫(xiě)自定義類(lèi)加載器時(shí),如果需要把加載請(qǐng)求委派給引導(dǎo)類(lèi)加載器去處理,那直接使用 null 代替即可。

  1. 擴(kuò)展類(lèi)加載器(Extension Class Loader)

這個(gè)類(lèi)加載器是在類(lèi) sun.misc.Launcher$ExtClassLoader 中以 Java 代碼的形式實(shí)現(xiàn)的。

它負(fù)責(zé)加載 <JAVA_HOME>\lib\ext 目錄中,或者被 java.ext.dirs 系統(tǒng)變量所指定的路徑中所有的類(lèi)庫(kù)

根據(jù) “擴(kuò)展類(lèi)加載器” 這個(gè)名稱(chēng),就可以推斷出這是一種 Java 系統(tǒng)類(lèi)庫(kù)的擴(kuò)展機(jī)制,JDK 的開(kāi)發(fā)團(tuán)隊(duì)允許用戶將具有通用性的類(lèi)庫(kù)放置在 ext 目錄里以擴(kuò)展 JavaSe 的功能,在JDK9之后,這種擴(kuò)展機(jī)制被模塊化帶來(lái)的天然的擴(kuò)展能力所取代。由于擴(kuò)展類(lèi)加載器是由 Java 代碼實(shí)現(xiàn)的,開(kāi)發(fā)者可以直接在程序中使用擴(kuò)展類(lèi)加載器來(lái)加載 Class 文件。

  1. 應(yīng)用程序類(lèi)加載器(Application Class Loader)

這個(gè)類(lèi)加載器由 sun.misc.LaunchersappClassloader 來(lái)實(shí)現(xiàn)。由于這個(gè)類(lèi)加載器是 Classloader 類(lèi)中的 getSystemClassloader() 方法的返回值,所以有些場(chǎng)合中也稱(chēng)它為“系統(tǒng)類(lèi)加載器”。

它負(fù)責(zé)加載用戶類(lèi)路徑 (ClassPath) 上所有的類(lèi)庫(kù),開(kāi)發(fā)者同樣可以直接在代碼中使用這個(gè)類(lèi)加載器。如果應(yīng)用程序中沒(méi)有自定義過(guò)自己的類(lèi)加載器,一般情況下這個(gè)就是程序中默認(rèn)的類(lèi)加載器。

除了這三種外,如果用戶有必要,還可以自定義來(lái)進(jìn)行擴(kuò)展:

image

2.4.2.2 雙親委派模型

上面的圖畫(huà)出來(lái)的關(guān)系,就被稱(chēng)為類(lèi)加載器的 “雙親委派模型( Parents DelegationModel)”。

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

雙親委派模型的工作過(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ì)嘗試自己去完成加載。

使用雙親委派模型來(lái)組織加載器之間的關(guān)系,一個(gè)顯而易見(jiàn)的好處就是:java 類(lèi)隨著類(lèi)加載器就具有了一種層級(jí)關(guān)系,比如 Object 類(lèi),不論哪個(gè)類(lèi)加載器加載他,都會(huì)委派給模型最頂端的啟動(dòng)類(lèi)加載器紀(jì)念性加載,因此 Object 類(lèi)在各種類(lèi)加載器環(huán)境里都能保證是同一個(gè)類(lèi),這樣 java 整個(gè)體系的最基礎(chǔ)行為就得到了保證。

雙親委派模型的實(shí)現(xiàn),可以在 java.lang.ClassLoader 的 loadClass() 方法里看到:

原文鏈接:https://www.cnblogs.com/lifegoeson/p/13621844.html

最后編輯于
?著作權(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)容

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