引言
上篇《初始Java虛擬機(jī)》文章中曾提及到:我們所編寫的Java代碼經(jīng)過編譯之后,會(huì)生成對應(yīng)的class
字節(jié)碼文件,而在程序啟動(dòng)時(shí)會(huì)通過類加載子系統(tǒng)將這些字節(jié)碼文件先裝載進(jìn)內(nèi)存,然后再交由執(zhí)行引擎執(zhí)行。本文中則會(huì)對Java虛擬機(jī)的類加載機(jī)制以及執(zhí)行引擎進(jìn)行全面分析。
一、初窺類加載機(jī)制及加載過程詳解
每個(gè)編寫出的.java
文件都存儲(chǔ)著需執(zhí)行的程序邏輯,經(jīng)過Java編譯器編譯后,會(huì)為每個(gè).java
文件生成對應(yīng)的.class
字節(jié)碼文件,.class
文件中則記錄著Java代碼轉(zhuǎn)換之后的虛擬機(jī)指令,每個(gè).class
文件開頭都有特定的標(biāo)識(shí)、魔數(shù)版本等信息。
當(dāng)JVM需要用到某個(gè)類時(shí),虛擬機(jī)會(huì)加載它的.class
文件,加載了相關(guān)的字節(jié)碼信息后,會(huì)為它創(chuàng)建對應(yīng)的Class
對象,而這個(gè)過程就被稱為類加載。但需額外注意的是:類加載機(jī)制只負(fù)責(zé)class
文件的加載,至于是否可以執(zhí)行,則是由執(zhí)行引擎決定。接著先看看類加載的過程。如下:
如上圖,類加載過程被分為三個(gè)步驟,五個(gè)階段,分別為加載、驗(yàn)證、準(zhǔn)備、解析以及初始化。加載、驗(yàn)證、準(zhǔn)備、初始化這四個(gè)階段的順序是確定的。但解析階段不一定,為了支持Java語言的運(yùn)行時(shí)綁定特性,在某些情況下可以在初始化階段之后再開始(也稱為動(dòng)態(tài)綁定或晚期綁定)。
1.1、加載步驟
加載階段是指通過完全限定名查找Class文件二進(jìn)制數(shù)據(jù)并將其加載進(jìn)內(nèi)存的過程。大體流程會(huì)分為三步:
- ①通過完全限定名查找定位
.class
文件,并獲取其二進(jìn)制字節(jié)流數(shù)據(jù) - ②把字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)換為運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
- ③在堆中間中為其創(chuàng)建一個(gè)
Class
對象,作為程序訪問這些數(shù)據(jù)的入口
1.2、連接步驟
連接步驟包含了驗(yàn)證、準(zhǔn)備、解析三個(gè)階段。這三個(gè)階段中,前兩個(gè)執(zhí)行順序是確定的,但解析階段不一定,可能會(huì)發(fā)生在初始化之后。
1.2.1、驗(yàn)證階段
驗(yàn)證階段主要用于確保被加載的Class
正確性,檢測Class
字節(jié)流中的數(shù)據(jù)是否符合虛擬機(jī)的要求,確保不會(huì)危害虛擬機(jī)自身安全。驗(yàn)證階段主要包括四種驗(yàn)證:文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證以及符號(hào)引用驗(yàn)證。
- ①文件格式驗(yàn)證:驗(yàn)證字節(jié)流是否符合
Class
文件格式的規(guī)范-
CA/FE/BA/BE
魔數(shù)驗(yàn)證 - 主次版本號(hào)驗(yàn)證
- 常量池中常量類型是否存在不被支持的類型驗(yàn)證
- 指向常量池中的索引是否有指定不存在或不符合類型的常量
-
- ②元數(shù)據(jù)驗(yàn)證:對字節(jié)碼描述的信息進(jìn)行語義分析,以保證其描述的信息符合Java語言規(guī)范的要求
- 類是否有父類,除了
Object
之外,所有的類都應(yīng)該有父類 - 類的父類是否繼承了不允許被繼承的類(被
final
修飾的類) - 如果這個(gè)類不是抽象類,是否實(shí)現(xiàn)了其父類或接口中要求實(shí)現(xiàn)的所有方法
- 類的字段/方法是否與父類的存在沖突。例如方法參數(shù)都一樣,返回值卻不同
- 類是否有父類,除了
- ③字節(jié)碼驗(yàn)證:通過數(shù)據(jù)流和控制流分析,確定程序語義合法且符合邏輯
- 對類的方法體進(jìn)行校驗(yàn)分析,保證在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)的行為
- 保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作,不會(huì)出現(xiàn)類似于在操作數(shù)棧放了一個(gè)
int
類型的數(shù)據(jù),讀取時(shí)卻按照long
類型加載到本地變量表中的情況 - 保障任何跳轉(zhuǎn)指令都不會(huì)跳轉(zhuǎn)到方法體之外的字節(jié)碼指令上
- ④符號(hào)引用驗(yàn)證:確保后續(xù)的解析動(dòng)作能正確執(zhí)行
- 通過字符串描述的全限定名是否能找到對應(yīng)的類
- 符號(hào)引用中的類、字段、方法的可訪問性是否可被當(dāng)前類訪問
1.2.2、準(zhǔn)備階段
準(zhǔn)備階段主要是為類中聲明的靜態(tài)變量分配內(nèi)存空間,并將其初始化成默認(rèn)值(零值)。不過值得注意的是:這個(gè)默認(rèn)值并非指在Java代碼中顯式賦予的值,而是指數(shù)據(jù)類型的默認(rèn)值。如static int i = 5;
這里只會(huì)將i
初始化為0。
在這里進(jìn)行的內(nèi)存分配僅包括類成員(static
成員),而實(shí)例成員則會(huì)在創(chuàng)建具體的Java對象時(shí)被一起分配在堆空間中。同時(shí)也不包含使用final
修飾的static
成員,因?yàn)?code>final在編譯的時(shí)候就會(huì)分配了,準(zhǔn)備階段會(huì)顯示初始化。
1.2.3、解析階段
解析階段主要是把類中對常量池內(nèi)的符號(hào)引用轉(zhuǎn)換為直接引用的過程。值得一提的是,解析操作往往會(huì)伴隨著JVM在執(zhí)行完初始化之后再執(zhí)行
符號(hào)引用:用一組符號(hào)來描述引用的目標(biāo),符號(hào)引用的字面量形式明確定義在《Java虛擬機(jī)規(guī)范》的Class文件格式中
直接引用:直接指向目標(biāo)的指針、相對偏移量或一個(gè)間接定位到目標(biāo)的句柄
而符號(hào)引用轉(zhuǎn)直接引用的過程,主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點(diǎn)限定符等7類符號(hào)引用進(jìn)行(分別對應(yīng)常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info
等)。
1.3、初始化步驟
初始化步驟中,主要是對類的靜態(tài)變量賦予正確的初始值,也就是在聲明靜態(tài)變量時(shí)指定的初始化值以及靜態(tài)代碼塊中的賦值。本質(zhì)上就是執(zhí)行類構(gòu)造器方法<clinit>()
的過程。而觸發(fā)初始化的情況分為六種,如下:
- ①遇到
new/getstatic/putstatic/invokestatic
這四條字節(jié)碼指令時(shí)觸發(fā)-
new
:使用new關(guān)鍵字創(chuàng)建一個(gè)實(shí)例對象 -
getstatic
:讀取一個(gè)靜態(tài)字段 -
putstatic
:設(shè)置一個(gè)靜態(tài)字段 -
invokestatic
:調(diào)用一個(gè)靜態(tài)方法
-
- ②對類型進(jìn)行反射調(diào)用,如果類型沒有經(jīng)過初始化,則會(huì)觸發(fā)初始化
- ③初始化一個(gè)類的時(shí)候,發(fā)現(xiàn)父類沒有初始化,則先觸發(fā)父類初始化
- ④虛擬機(jī)啟動(dòng)時(shí),需指定一個(gè)要執(zhí)行的主類(包含main方法的那個(gè)類),虛擬機(jī)會(huì)初始化這個(gè)主類(如SpringBoot的啟動(dòng)類)
- ⑤當(dāng)使用JDK7中新加入的動(dòng)態(tài)語言支持時(shí),如果一個(gè)
java.lang.invoke.MethodHandler
實(shí)例最后的解析結(jié)果為REF_getStatic,REF_putStatic,REF_invokeStatic,REF_newInvokeSpecial
四種類型的方法句柄,并且這個(gè)方法對應(yīng)的類沒有進(jìn)行初始化,則先觸發(fā)其初始化 - ⑥當(dāng)一個(gè)接口中定了JDK8新加入的默認(rèn)方法時(shí),如果這個(gè)接口的實(shí)現(xiàn)類發(fā)生了初始化,要先將接口進(jìn)行初始化
在初始化階段中,有且只有這六種情況會(huì)觸發(fā)類的初始化,這些情況被稱為主動(dòng)引用。除了以上幾種情況外,其他使用類的方式被看做是對類的被動(dòng)引用,不會(huì)導(dǎo)致類的初始化。比如在子類中調(diào)用父類的靜態(tài)字段、定義該類的數(shù)組方式引用、調(diào)用該類的常量等情況都不會(huì)觸發(fā)類進(jìn)行初始化。
同時(shí),一個(gè)類被觸發(fā)初始化時(shí),在它進(jìn)行初始化的時(shí)候,大體步驟如下:
- 如果類還未被加載、連接則先進(jìn)行加載、連接步驟
- 如果當(dāng)前類存在直接父類未被初始化,則先初始化直接父類
- 構(gòu)造器方法中指令按照語句在源文中出現(xiàn)的順序執(zhí)行
如果大家對于類初始化特別感興趣,也可以自己通過案例來模擬出各種主動(dòng)引用以及被動(dòng)引用的情況觀察。
1.4、使用、卸載
當(dāng)一個(gè)類完整的經(jīng)過了類加載過程之后,在內(nèi)存中已經(jīng)生成了Class對象,同時(shí)在Java程序中已經(jīng)通過它開始創(chuàng)建實(shí)例對象使用時(shí),該階段被稱為使用階段。
而當(dāng)一個(gè)Class對象不再被任何一處位置引用,即不可觸及時(shí),Class就會(huì)結(jié)束生命周期,該類加載的數(shù)據(jù)也會(huì)被卸載。但是注意:
Java虛擬機(jī)自帶的類加載器加載的類,在虛擬機(jī)的生命周期中始終不會(huì)被卸載,因?yàn)镴VM始終會(huì)保持與這些類加載器的引用,而這些類加載器也會(huì)始終保持著自己加載的Class對象的引用,所以對于虛擬機(jī)而言,這些Class對象始終是可以被觸及的。不過由用戶自定義的類加載器加載的類是可以被卸載的。
二、JVM的類加載器(ClassLoader)分析
類加載器的任務(wù)是,根據(jù)一個(gè)類的全限定名讀取它的二進(jìn)制字節(jié)流數(shù)據(jù)后,將其加載到內(nèi)存中并轉(zhuǎn)換為一個(gè)與該類對應(yīng)的Class對象。而虛擬機(jī)提供了三種類加載器,同時(shí)也可以自己實(shí)現(xiàn),如下:
2.1、Bootstrap 引導(dǎo)類加載器
引導(dǎo)類加載器在有些地方也被稱為啟動(dòng)類加載器或根類加載器,但其實(shí)都是一個(gè)意思,都是在指BootstrapClassLoader
。引導(dǎo)類加載器是使用C++語言實(shí)現(xiàn)的,是JVM自身的一部分,主要負(fù)責(zé)將<JAVA_HOME>\lib
路徑下的核心類庫或-Xbootclasspath
參數(shù)指定的路徑下的jar包加載到內(nèi)存中。
注意:因?yàn)镴VM是通過全限定名加載類庫的,所以,如果你的文件名不被虛擬機(jī)識(shí)別,就算你把jar包丟入到lib目錄下,引導(dǎo)類加載器也并不會(huì)加載它。出于安全考慮,Bootstrap啟動(dòng)類加載器只加載包名為java、javax、sun等開頭的類文件。
引導(dǎo)類加載器只為JVM提供加載服務(wù),開發(fā)者不能直接使用它來加載自己的類。
2.2、Extension 拓展類加載器
這個(gè)類加載器是由sun公司實(shí)現(xiàn)的,位于HotSpot
源碼目錄中的sun.misc.Launcher$ExtClassLoader
位置。它主要負(fù)責(zé)加載<JAVA_HOME>\lib\ext
目錄下或者由系統(tǒng)變量-Djava.ext.dir
指定位路徑中的類庫。它可以直接被開發(fā)者使用。
2.3、Application 系統(tǒng)類加載器
也被稱為應(yīng)用程序類加載器,也是由sun公司實(shí)現(xiàn)的,位于HotSpot
源碼目錄中的sun.misc.Launcher$AppClassLoader
位置。它負(fù)責(zé)加載系統(tǒng)類路徑java -classpath
或-D java.class.path
指定路徑下的類庫,也就是經(jīng)常用到的classpath
路徑。應(yīng)用程序類加載器也可以直接被開發(fā)者使用。
一般情況下,該類加載器是程序的默認(rèn)類加載器,我們可以通過ClassLoader.getSystemClassLoader()方法可以直接獲取到它。
2.4、User 自定義類加載器
在Java程序中,運(yùn)行時(shí)一般都是通過如上三種類加載器相互配合執(zhí)行的,當(dāng)然,如果有特殊的加載需求也可以自定義類加載器,通過繼承ClassLoader
類實(shí)現(xiàn)(稍后分析)。
2.5、四種類加載器之間的關(guān)系
如上分析的類加載器關(guān)系鏈如下:
Bootstrap
引導(dǎo)類加載器 →Extension
拓展類加載器 →Application
系統(tǒng)類加載器 →User
自定義類加載器
Bootstrap
類加載器是在JVM啟動(dòng)時(shí)初始化的,它會(huì)負(fù)責(zé)加載ExtClassLoader
,并將其父加載器設(shè)置為BootstrapClassLoader
。BootstrapClassLoader
加載完ExtClassLoader
后會(huì)接著加載AppClassLoader
系統(tǒng)類加載器,并將其父加載器設(shè)置為ExtClassLoader
拓展類加載器。而自己定義的類加載器會(huì)由系統(tǒng)類加載器加載,加載完成后,AppClassLoader
會(huì)成為它們的父加載器。
但值得注意的是:類加載器之間并不存在相互繼承或包含關(guān)系,從上至下僅存在父加載器的層級(jí)引用關(guān)系。
下面我們通過Java代碼來簡單剖析一下類加載器之間的關(guān)系,案例如下:
// 自定義類加載器
public class ClassLoaderDemo extends ClassLoader {
public static void main(String[] args){
ClassLoaderDemo classLoader = new ClassLoaderDemo();
System.out.println("自定義加載器:" +
classLoader);
System.out.println("自定義加載器的父類加載器:" +
classLoader.getParent());
System.out.println("Java程序系統(tǒng)默認(rèn)的加載器:" +
ClassLoader.getSystemClassLoader());
System.out.println("系統(tǒng)類加載器的父加載器:" +
ClassLoader.getSystemClassLoader().getParent());
System.out.println("拓展類加載器的父加載器:"
+ ClassLoader.getSystemClassLoader().getParent().getParent());
}
}
輸出結(jié)果如下:
自定義加載器:com.sixstarServiceOrder.ClassLoaderDemo@6d5380c2
自定義加載器的父類加載器:sun.misc.Launcher$AppClassLoader@18b4aac2
Java程序系統(tǒng)默認(rèn)的加載器:sun.misc.Launcher$AppClassLoader@18b4aac2
系統(tǒng)類加載器的父加載器:sun.misc.Launcher$ExtClassLoader@45ff54e6
拓展類加載器的父加載器:null
因?yàn)?code>BootstrapClassLoader是由C++實(shí)現(xiàn)的,所以在獲取ExtClassLoader
的父類加載器時(shí),獲取到的結(jié)果為null。
2.6、類加載器小結(jié)
JVM的類加載機(jī)制是按需加載的模式運(yùn)行的,也就是代表著:所有類并不會(huì)在程序啟動(dòng)時(shí)全部加載,而是當(dāng)需要用到某個(gè)類發(fā)現(xiàn)它未加載時(shí),才會(huì)去觸發(fā)加載的過程。
Java中的類加載器會(huì)被組織成存在父子級(jí)關(guān)系的層級(jí)結(jié)構(gòu)。同時(shí),類加載器之間也存在代理模式,當(dāng)一個(gè)類需要被加載時(shí),首先會(huì)依次根據(jù)層級(jí)結(jié)構(gòu)檢查自己父加載器是否對這個(gè)類進(jìn)行了加載,如果父層已經(jīng)裝載了則可以直接使用,反之,如果未被裝載則依次從上至下詢問,是否在可加載范圍,是否允許被當(dāng)前層級(jí)的加載器加載,如果可以則加載。
每個(gè)類加載器都擁有一個(gè)自己的命名空間,命名空間的作用是用于存儲(chǔ)被自身加載過的所有類的全限定名(Fully Qualified Class Name
) ,子類加載器查找父類加載器是否加載過一個(gè)類時(shí),就是通過類的權(quán)限定名在父類的命名空間中進(jìn)行匹配。而Java虛擬機(jī)判斷兩個(gè)類是否相同的基準(zhǔn)就是通過ClassLoaderId + PackageName + ClassName
進(jìn)行判斷,也就代表著,Java程序運(yùn)行過程中,是允許存在兩個(gè)包名和類名完全一致的class
的,只需要使用不同的類加載器加載即可,這也就是Java類加載器存在的隔離性問題,而Java為了解決這個(gè)問題,JVM引入了雙親委派機(jī)制(稍后分析)。
剛剛提到過,子類加載器可以檢查父類加載器中加載的類,但這個(gè)是不可逆的,也就代表著父類加載器是不可以查找子類加載器加載的類,存在可見性限制。同時(shí),被Bootstrap、Ext、APP
三個(gè)類加載器加載的類是不可以被卸載的(前面分析過),但可以刪除當(dāng)前的類裝載器,然后創(chuàng)建一個(gè)新的類裝載器裝載。
篇外:其實(shí)當(dāng)Java在加載類時(shí),會(huì)分為顯式加載和隱式加載兩種,顯式加載指的是開發(fā)者手動(dòng)通過調(diào)用
ClassLoader
加載一個(gè)類,比如Class.forName(name)
或obj.getClass().getClassLoader().loadClass()
方式加載class
對象。而隱式加載則是指不會(huì)在程序中明確的指定加載某個(gè)類,屬于被動(dòng)式加載,比如在加載某個(gè)類時(shí),該類中引用了另外一個(gè)類的對象時(shí),JVM就會(huì)去自動(dòng)加載另外一個(gè)類,而這種被動(dòng)加載方式就被稱為“隱式加載”。
三、類加載子系統(tǒng)中的雙親委派機(jī)制
前面提到過,為了解決類加載器的隔離性問題,JVM引入了雙親委派機(jī)制(1.2的時(shí)候引入的),而雙親委派的核心思想在于兩點(diǎn):
- ①自下向上檢查類是否已經(jīng)被加載
- ②從上至下嘗試加載類
下面來簡單感受一下雙親委派機(jī)制的加載過程。
3.1、雙親委派類加載過程
- ①當(dāng)
App
嘗試加載一個(gè)類時(shí),它不會(huì)直接嘗試加載這個(gè)類,首先會(huì)在自己的命名空間中查詢是否已經(jīng)加載過這個(gè)類,如果沒有會(huì)先將這個(gè)類加載請求委派給父類加載器Ext
完成 - ②當(dāng)
Ext
嘗試加載一個(gè)類時(shí),它也不會(huì)直接嘗試加載這個(gè)類,也會(huì)在自己的命名空間中查詢是否已經(jīng)加載過這個(gè)類,沒有的話也會(huì)先將這個(gè)類加載請求委派給父類加載器Bootstrap
完成 - ③如果
Bootstrap
加載失敗,也就是代表著:這個(gè)需要被加載的類不在Bootstrap
的加載范圍內(nèi),那么Bootstrap
會(huì)重新將這個(gè)類加載請求交由子類加載器Ext
完成 - ④如果
Ext
加載失敗,代表著這個(gè)類也不在Ext
的加載范圍內(nèi),最后會(huì)重新將這個(gè)類加載請求交給子類加載器App
完成 - ⑤如果
App
加載器也加載失敗,就代表這個(gè)類根據(jù)全限定名無法查找到,則會(huì)拋出ClassNotFoundException
異常
3.2、雙親委派機(jī)制的優(yōu)勢
從上述的過程中可以很直觀的感受到:當(dāng)JVM嘗試加載一個(gè)類時(shí),通常最底層的類加載器接收到了類加載請求之后,會(huì)先交由自己的上層類加載器完成,如下:
OK~,那采用這種模式優(yōu)勢在哪兒呢?
采用雙親委派模式的好處在于:Java類隨著它的類加載器存在了一種優(yōu)先級(jí)的層次關(guān)系,這樣做的優(yōu)勢在于,可以避免一個(gè)類在不同層級(jí)的類加載器中重復(fù)加載,如果父類加載器已經(jīng)加載過該類了,那么就不需要子類加載器再加載一次。其次,也可以保障Java核心類的安全性問題,比如通過網(wǎng)絡(luò)傳輸過來一個(gè)
java.lang.String
類,需要被加載時(shí),通過這種雙親委派的方式,最終找到Bootstrap
加載器后,發(fā)現(xiàn)該類已經(jīng)被加載,從而就不會(huì)再加載傳輸過來的java.lang.String
類,而是直接返回Bootstrap
加載的String.class
。這樣可以有效防止Java的核心API類在運(yùn)行時(shí)被篡改,從而保證所有子類共享同一基礎(chǔ)類,減少性能開銷和安全隱患問題。
3.3、Java中雙親委派的實(shí)現(xiàn)原理
前面大致的探討了Java的類加載機(jī)制以及雙親委派機(jī)制,接著再從代碼層面了解一下Java中雙親委派模式的實(shí)現(xiàn)以及Java中定義的一些類加載器。
在Java中,所有的類加載器都間接的繼承自ClassLoader
類,包括Ext、App
類加載器(Bootstrap
除外,因?yàn)樗荂++實(shí)現(xiàn)的),如下:
// sun.misc.Launcher類
public class Launcher {
// sun.misc.Launcher類 → 構(gòu)造器
public Launcher(){
Launcher.ExtClassLoader var1;
try {
// 會(huì)先初始化Ext類加載器并創(chuàng)建ExtClassLoader
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError(
"Could not create extension class loader", var10);
}
try {
// 再創(chuàng)建AppClassLoader并把Ext作為父加載器傳遞給App
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader");
}
// 將APP類加載器設(shè)置為線程上下文類加載器(稍后分析)
Thread.currentThread().setContextClassLoader(loader);
// 省略......
}
// sun.misc.Launcher類 → ExtClassLoader內(nèi)部類
static class ExtClassLoader extends URLClassLoader {
// ExtClassLoader內(nèi)部類 → 構(gòu)造器
public ExtClassLoader(File[] var1) throws IOException {
// 在Ext初始化時(shí),父類構(gòu)造器會(huì)被設(shè)置為null
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this)
.initLookupCache(this);
}
}
// sun.misc.Launcher類 → AppClassLoader內(nèi)部類
static class AppClassLoader extends URLClassLoader {}
}
// java.net.URLClassLoader類
public class URLClassLoader extends SecureClassLoader
implements Closeable {}
// java.security.SecureClassLoader類
public class SecureClassLoader extends ClassLoader {}
如上源碼,Ext、App
類加載器都是sun.misc.Launcher
類的內(nèi)部類,而Launcher
在初始化時(shí)會(huì)首先創(chuàng)建Ext
類加載器,而在初始化Ext
時(shí),它的構(gòu)造器中會(huì)強(qiáng)行將其父類加載器設(shè)置為null
。創(chuàng)建完成Ext
類加載器之后,會(huì)緊接著再創(chuàng)建App
類加載器,同時(shí)在創(chuàng)建AppClassLoader
的時(shí)候會(huì)將Ext
類加載器設(shè)置為App
類加載器的父類加載器。
Ext、App
類加載器都繼承了URLClassLoader
類,該類主要是用于讀取各種jar
包、本地class
以及網(wǎng)絡(luò)傳遞的class
文件,通過找到它們的字節(jié)碼,然后再將其讀取成字節(jié)流,最后通過defineClass()
方法創(chuàng)建類的Class對象。而URLClassLoader
類繼承了SecureClassLoader
類,該類也作為了ClassLoader
類的拓展類,新增了幾個(gè)對代碼源的位置及其證書的驗(yàn)證以及權(quán)限定義類驗(yàn)證(主要指對class
源碼的訪問權(quán)限)的方法,一般我們不會(huì)直接跟這個(gè)類打交道,更多是與它的子類URLClassLoader
有所關(guān)聯(lián)。
總而言之,Ext、App
類加載器都間接的繼承了ClassLoader
類,ClassLoader
類作為Java類加載機(jī)制的頂層設(shè)計(jì)類,它是一個(gè)抽象類,下面來簡單的看看ClassLoader
,如下:
// ClassLoader類 → loadClass()方法
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 加鎖
synchronized (getClassLoadingLock(name)) {
// 先嘗試通過全限定名從自己的命名空間中查找該Class對象
Class<?> c = findLoadedClass(name);
// 如果找到了則不需要加載了,如果==null,開始類加載
if (c == null) {
long t0 = System.nanoTime();
try {
// 先將類加載任務(wù)委托自己的父類加載器完成
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果父類加載器為null,代表當(dāng)前已經(jīng)是ext加載器了
// 那么則將任務(wù)委托給Bootstrap加載器加載
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 處理異常,拋出異常
}
if (c == null) {
// 如果都沒有找到,則通過自定義實(shí)現(xiàn)的findClass
// 去查找并加載
long t1 = System.nanoTime();
c = findClass(name);
// 這是記錄類加載相關(guān)數(shù)據(jù)的(比如耗時(shí)、類加載數(shù)量等)
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
// 是否需要在加載時(shí)進(jìn)行解析,如果是則觸發(fā)解析操作
if (resolve) {
resolveClass(c);
}
// 返回加載后生成的Class對象
return c;
}
}
// ClassLoader類 → findClass()方法
protected Class<?> findClass(String name)
throws ClassNotFoundException {
// 直接拋出異常(這個(gè)方法是留給子類重寫的)
throw new ClassNotFoundException(name);
}
// ClassLoader類 → defineClass()方法
protected final Class<?> defineClass(String name, byte[] b,
int off, int len) throws ClassFormatError
{
// 調(diào)用了defineClass方法,
// 將字節(jié)數(shù)組b的內(nèi)容轉(zhuǎn)換為一個(gè)Java類
return defineClass(name, b, off, len, null);
}
// ClassLoader類 → resolveClass()方法
protected final void resolveClass(Class<?> c) {
// 調(diào)用本地(navite)方法,解析一個(gè)類
resolveClass0(c);
}
// ClassLoader類 → getParent()方法
@CallerSensitive
public final ClassLoader getParent() {
// 如果當(dāng)前類加載器的父類加載器為空,則直接返回null
if (parent == null)
return null;
// 如果不為空則先獲取安全管理器
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// 然后檢查權(quán)限后返回當(dāng)前classLoader的父類加載器
checkClassLoaderPermission(parent,
Reflection.getCallerClass());
}
return parent;
}
上述簡單的羅列了一些ClassLoader
類的關(guān)鍵方法,具體作用如下:
-
loadClass(name,resolve)
:加載名稱為name
的類,加載后返回Class
對象實(shí)例 -
findClass(name)
:查找名稱為name
的類,返回是一個(gè)Class
對象實(shí)例(該方法是留給子類重寫覆蓋的,在loadClass
中,在父類加載器加載失敗的情況下會(huì)調(diào)用該方法完成類加載,這樣可以保證自定義的類加載器也符合雙親委托模式) -
defineClass(name,b,off,len)
:將字節(jié)流b
轉(zhuǎn)換為一個(gè)Class
對象 -
resolveClass(c)
:使用該方法可以對加載完生成的Class
對象同時(shí)進(jìn)行解析操作 -
getParent()
:獲取當(dāng)前類加載器的父類加載器
OK,簡單的看了一下Java中類加載器之間的關(guān)系之后,怎么去分析雙親委派的實(shí)現(xiàn)呢?其實(shí)雙親委派模型的實(shí)現(xiàn)邏輯全在于loadClass()
方法,而ExtClassLoader
加載器是沒有重寫loadClass()
方法,AppClassLoader
加載器雖然重寫了loadClass()
方法,但其內(nèi)部最終還是調(diào)用父類的loadClass()
方法,如下:
// sun.misc.Launcher類 → AppClassLoader內(nèi)部類 → loadClass()方法
public Class loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
int i = name.lastIndexOf('.');
if (i != -1) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPackageAccess(name.substring(0, i));
}
}
// 依舊調(diào)用的是父類loadClass()方法
return (super.loadClass(name, resolve));
}
所以無論是ExtClassLoader
還是AppClassLoader
加載器,其本身都未打破ClassLoader.loadClass()
方法中定義的雙親委派邏輯,Bootstrap、Ext、App
這些JVM自帶的類加載器都默認(rèn)會(huì)遵守雙親委派模型。
四、自定義類加載器分析及實(shí)戰(zhàn)
通過前面的分析不難得知,如果需要自定義類加載器,那么只需要繼承ClassLoader
類即可,但繼承ClassLoader
需要自己重寫findClass()
方法并編寫加載邏輯。所以如果一般沒有太過復(fù)雜的需求,可以直接繼承URLClassLoader
類,可以省略自己編寫findClass
方法以及文件加載轉(zhuǎn)換成字節(jié)碼流的步驟,使自定義類加載器編寫更加簡潔。那什么情況下時(shí),我們需要自定義類加載器呢?
- ①當(dāng)
class
文件不在classpath
路徑下時(shí),需要自定義類加載器加載特定路徑下的class
- ②當(dāng)一個(gè)
class
文件是通過網(wǎng)絡(luò)傳輸過來的并經(jīng)過了加密處理,需要首先對class
文件做了對應(yīng)的解密處理后再加載到內(nèi)存中時(shí),需要自定義類加載器- 案例:比如我之前寫的一個(gè)項(xiàng)目,運(yùn)維子平臺(tái)有個(gè)需求需要一個(gè)編寫Java代碼的終端,那對于這種情況就需要將運(yùn)維平臺(tái)中編寫的
class
文件經(jīng)過網(wǎng)絡(luò)傳輸過來,然后對其類進(jìn)行加載
- 案例:比如我之前寫的一個(gè)項(xiàng)目,運(yùn)維子平臺(tái)有個(gè)需求需要一個(gè)編寫Java代碼的終端,那對于這種情況就需要將運(yùn)維平臺(tái)中編寫的
- ③線上環(huán)境不能停機(jī)時(shí),要?jiǎng)討B(tài)更改某塊代碼,這種情況下需要自定義類加載器
- 比如:當(dāng)需要實(shí)現(xiàn)熱部署功能時(shí)(一個(gè)class文件通過不同的類加載器產(chǎn)生不同class對象從而實(shí)現(xiàn)熱部署功能)
4.1、自定義類加載器實(shí)戰(zhàn)
案例:運(yùn)維子平臺(tái)有個(gè)需求,需要一個(gè)編寫Java代碼的終端,那對于這種情況就需要將運(yùn)維平臺(tái)中編寫的
class
文件經(jīng)過加密后,通過網(wǎng)絡(luò)傳輸過來,然后對其類的字節(jié)碼數(shù)據(jù)進(jìn)行解密后再加載。源碼如下:
// 運(yùn)維終端類加載器
public class OpsClassLoader extends ClassLoader {
// 接收到的class文件本地的存儲(chǔ)位置
private String rootDirPath;
// 構(gòu)造器
public OpsClassLoader(String rootDirPath) {
this.rootDirPath = rootDirPath;
}
// 讀取Class字節(jié)流并解密的方法
private byte[] getClassDePass(String className) throws IOException {
String classpath = rootDirPath + className;
// 模擬文件讀取過程.....
FileInputStream fis = new FileInputStream(classpath);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
int n = 0;
byte[] buffer = new byte[bufferSize];
while ((n = fis.read(buffer)) != -1)
// 模擬數(shù)據(jù)解密過程.....
baos.write(buffer, 0, n);
byte[] data = baos.toByteArray();
// 模擬保存解密后的數(shù)據(jù)....
return data;
}
// 重寫了父類的findClass方法
@SneakyThrows
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 讀取指定的class文件
byte[] classData = getClassDePass(name);
// 如果沒讀取到數(shù)據(jù),拋出類不存在的異常
if (classData == null)
throw new ClassNotFoundException();
// 通過調(diào)用defineClass方法生成Class對象
return defineClass(name,classData,0,classData.length);
}
}
在如上源碼中,我們通過了getClassDePass()
方法讀取了網(wǎng)絡(luò)傳輸過來存儲(chǔ)到本地的class
文件的字節(jié)流數(shù)據(jù),并對讀取到的數(shù)據(jù)做了對應(yīng)的解密處理(模擬),然后通過重寫了父類的ClassLoader.findClass()
方法,利用defineClass()
方法在JVM內(nèi)存中生成了最終的Class
對象。
當(dāng)然,如果你想代碼更簡潔,你也可以通過繼承
URLClassLoader
類實(shí)現(xiàn)。
4.2、熱部署機(jī)制原理分析
相信大家對于熱部署機(jī)制都并不陌生,在熱部署機(jī)制沒有出現(xiàn)時(shí),往往我們稍微更改了一丟丟的Java代碼,就需要對整個(gè)項(xiàng)目重啟之后才可生效。這毫無疑問是非常痛苦的,尤其是早些年用Eclipse、MyEclipse
編輯器開發(fā)的時(shí)代,很多時(shí)候因?yàn)槟銓懲甏a沒有按Ctrl+S
保存代碼就趕著調(diào)試,急匆匆的啟動(dòng)了你的項(xiàng)目,到頭來發(fā)現(xiàn)沒有保存自己的新代碼,又需要再次保存后重啟項(xiàng)目,這種糟糕的經(jīng)歷大部分資歷較老的開發(fā)者應(yīng)該都遇到過。
所以在熱部署機(jī)制未出現(xiàn)之前,一改就需重啟的時(shí)代,尤其是一些大項(xiàng)目,啟動(dòng)都需花費(fèi)幾小時(shí),無疑是令人糟心的體驗(yàn)。而熱部署機(jī)制出現(xiàn)之后,我們可以發(fā)現(xiàn),當(dāng)我們在Java程序運(yùn)行過程中,動(dòng)態(tài)的修改了某個(gè)類的代碼保存后,程序會(huì)自動(dòng)加載更新代碼,這是如何實(shí)現(xiàn)的呢?在之前的類加載機(jī)制中,我們分析得知:全限定名相同的一個(gè)類被加載過之后,第二次需要用到該類時(shí),會(huì)直接在類加載器的命名空間(可以理解為緩存)中進(jìn)行查找,而不會(huì)二次加載此類,而強(qiáng)制指定同一個(gè)類加載器二次加載同一個(gè)類時(shí),會(huì)拋出異常。所以一般類被加載一次之后,就算某個(gè)類的class
文件發(fā)生了改變,JVM也不會(huì)再次加載它。
而所謂的熱部署機(jī)制的實(shí)現(xiàn)其實(shí)比較簡單,就是通過利用不同的類加載器,去加載更改后的
class
文件,從而在內(nèi)存中創(chuàng)建出兩個(gè)不同的Class
對象。從而達(dá)到類文件更改后可以生效的目的。
五、雙親委派破壞者 - 線程上下文類加載器
在Java中,官方為我們提供了很多SPI接口,例如JDBC、JBI、JNDI等。這類SPI接口,官方往往只會(huì)定義規(guī)范,具體的實(shí)現(xiàn)則是由第三方來完成的,比如JDBC,不同的數(shù)據(jù)庫廠商都需自己根據(jù)JDBC接口的定義進(jìn)行實(shí)現(xiàn)。
而這些SPI接口直接由Java核心庫來提供,一般位于rt.jar
包中,而第三方實(shí)現(xiàn)的具體代碼庫則一般被放在classpath
的路徑下。而此時(shí)問題來了:
位于
rt.jar
包中的SPI接口,是由Bootstrap類加載器完成加載的,而classpath
路徑下的SPI實(shí)現(xiàn)類,則是App
類加載器進(jìn)行加載的。但往往在SPI接口中,會(huì)經(jīng)常調(diào)用實(shí)現(xiàn)者的代碼,所以一般會(huì)需要先去加載自己的實(shí)現(xiàn)類,但實(shí)現(xiàn)類并不在Bootstrap類加載器的加載范圍內(nèi),而經(jīng)過前面的雙親委派機(jī)制的分析,我們已經(jīng)得知:子類加載器可以將類加載請求委托給父類加載器進(jìn)行加載,但這個(gè)過程是不可逆的。也就是父類加載器是不能將類加載請求委派給自己的子類加載器進(jìn)行加載的,所以此時(shí)就出現(xiàn)了這個(gè)問題:如何加載SPI接口的實(shí)現(xiàn)類?答案是打破雙親委派模型。
SPI(Service Provider Interface):Java的SPI機(jī)制,其實(shí)就是可拔插機(jī)制。在一個(gè)系統(tǒng)中,往往會(huì)被分為不同的模塊,比如日志模塊、JDBC模塊等,而每個(gè)模塊一般都存在多種實(shí)現(xiàn)方案,如果在Java的核心庫中,直接以硬編碼的方式寫死實(shí)現(xiàn)邏輯,那么如果要更換另一種實(shí)現(xiàn)方案,就需要修改核心庫代碼,這就違反了可拔插機(jī)制的原則。為了避免這樣的問題出現(xiàn),就需要一種動(dòng)態(tài)的服務(wù)發(fā)現(xiàn)機(jī)制,可以在程序啟動(dòng)過程中,動(dòng)態(tài)的檢測實(shí)現(xiàn)者。而SPI中就提供了這么一種機(jī)制,專門為某個(gè)接口尋找服務(wù)實(shí)現(xiàn)的機(jī)制。如下:
當(dāng)?shù)谌綄?shí)現(xiàn)者提供了服務(wù)接口的一種實(shí)現(xiàn)之后,在jar包的META-INF/services/目錄里同時(shí)創(chuàng)建一個(gè)以服務(wù)接口命名的文件,該文件就是實(shí)現(xiàn)該服務(wù)接口的實(shí)現(xiàn)類。而當(dāng)外部程序裝配這個(gè)模塊的時(shí)候,就能通過該jar包META-INF/services/里的配置文件找到具體的實(shí)現(xiàn)類名,并裝載實(shí)例化,完成模塊的注入。基于這樣一個(gè)約定就能很好的找到服務(wù)接口的實(shí)現(xiàn)類,而不需要再代碼里制定。同時(shí),JDK官方也提供了一個(gè)查找服務(wù)實(shí)現(xiàn)者的工具類:java.util.ServiceLoader。
線程上下文類加載器就是雙親委派模型的破壞者,可以在執(zhí)行線程中打破雙親委派機(jī)制的加載鏈關(guān)系,從而使得程序可以逆向使用類加載器。
那線程上下文類加載器又是如何打破雙親委派模型使得程序可以逆向使用類加載器的呢?接下來通過分析JDBC驅(qū)動(dòng)的源碼一窺究竟。
5.1、JDBC角度分析線程上下文類加載器
我們先來看看Java中SPI定義的一個(gè)核心類:DriverManager
,該類位于rt.jar
包中,是Java中用于管理不同數(shù)據(jù)庫廠商實(shí)現(xiàn)的驅(qū)動(dòng),同時(shí)這些各廠商實(shí)現(xiàn)的Driver
驅(qū)動(dòng)類,都繼承自Java的核心類java.sql.Driver
,如MySQL的com.mysql.cj.jdbc.Driver
的驅(qū)動(dòng)類。先看看DriverManager
的源碼,如下:
// rt.jar包 → DriverManager類
public class DriverManager {
// .......
// 靜態(tài)代碼塊
static {
// 加載并初始化驅(qū)動(dòng)
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
// DriverManager類 → loadInitialDrivers()方法
private static void loadInitialDrivers() {
// 先讀取系統(tǒng)屬性 jdbc.drivers
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//通過ServiceLoader類查找驅(qū)動(dòng)類的文件位置并加載
ServiceLoader<Driver> loadedDrivers =
ServiceLoader.load(Driver.class);
//省略......
}
});
//省略......
}
觀察如上源碼,在DriverManager
類的靜態(tài)代碼塊中調(diào)用了loadInitialDrivers()
方法,該方法中,會(huì)通過ServiceLoader
查找服務(wù)接口的實(shí)現(xiàn)類。前面分析Java的SPI
機(jī)制時(shí),曾提到過:Java的SPI
存在一種動(dòng)態(tài)的服務(wù)發(fā)現(xiàn)機(jī)制,在程序啟動(dòng)時(shí),會(huì)自動(dòng)去jar
包中的META-INF/services/
目錄查找以服務(wù)命名的文件,mysql-connector-java-6.0.6.jar
包文件目錄如下:
觀察如上工程結(jié)構(gòu),我們明確可以看到,在MySQL的
jar
包中存在一個(gè)META-INF/services/
目錄,而在該目錄下,存在一個(gè)java.sql.Driver
文件,該文件中指定了MySQL
驅(qū)動(dòng)Driver
類的路徑,該類源碼如下:
// com.mysql.cj.jdbc.Driver類
public class Driver extends NonRegisteringDriver
implements java.sql.Driver {
public Driver() throws SQLException {
}
// 省略.....
}
可以看到,該類是實(shí)現(xiàn)了Java定義的SPI接口java.sql.Driver
的,所以在啟動(dòng)時(shí),SPI
的動(dòng)態(tài)服務(wù)發(fā)現(xiàn)機(jī)制可以發(fā)現(xiàn)指定的位置下的驅(qū)動(dòng)類。
在MySQL6.0之后的jar包中,遺棄了之前的com.mysql.jdbc.Driver驅(qū)動(dòng),而是使用com.mysql.cj.jdbc.Driver取而代之,因?yàn)楹笳卟恍枰僮约和ㄟ^
Class.forName("com.mysql.jdbc.Driver")
這種方式手動(dòng)注冊驅(qū)動(dòng),全部都可以交由給SPI機(jī)制處理。
最終來看看SPI機(jī)制是如何加載對應(yīng)實(shí)現(xiàn)類的,ServiceLoader.load()
源碼如下:
// ServiceLoader類 → load()方法
public static <S> ServiceLoader<S> load(Class<S> service) {
// 獲取線程上下文類加載器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 使用線程上下文類加載器對驅(qū)動(dòng)類進(jìn)行加載
return ServiceLoader.load(service, cl);
}
通過如上源碼可以清晰的看見:最終是通過Thread.currentThread().getContextClassLoader()
獲取的當(dāng)前執(zhí)行線程的線程上下文類加載器對SPI接口的實(shí)現(xiàn)類進(jìn)行了加載。OK~,在前面我們分析Java中的雙親委派實(shí)現(xiàn)時(shí),曾提到了Ext、App
類加載器都是Launcher
類的內(nèi)部類,Ext、App
類加載器的初始化操作都是在Launcher
的構(gòu)造函數(shù)中完成的,同時(shí),在該構(gòu)造函數(shù)中,Ext、App
初始化完成后,會(huì)執(zhí)行下面這句代碼:
Thread.currentThread().setContextClassLoader(loader);
通過如上這句代碼,在Launcher
的構(gòu)造函數(shù)中,會(huì)將已經(jīng)創(chuàng)建好的AppClassLoader
系統(tǒng)類加載器設(shè)置為默認(rèn)的線程上下文類加載器。
分析到了這個(gè)地方后,可能有部分小伙伴會(huì)有些繞,我們稍微梳理一下總體流程:
Java程序啟動(dòng) → JVM初始化C++編寫的
Bootstrap
啟動(dòng)類加載器 →Bootstrap
加載Java核心類(核心類中包含Launcher
類) →Bootstrap
加載Launcher
類,其中觸發(fā)Launcher
構(gòu)造函數(shù) →Bootstrap
執(zhí)行Launcher
構(gòu)造函數(shù)的邏輯 →Bootstrap
初始化并創(chuàng)建Ext、App
類加載器 →Launcher
類的構(gòu)造函數(shù)中將Ext
設(shè)置為App
的父類加載器 → 同時(shí)再將App
設(shè)置為默認(rèn)的線程上下文類加載器 →Bootstrap
繼續(xù)加載其他Java核心類(如:SPI接口) → SPI接口中調(diào)用了第三方實(shí)現(xiàn)類的方法 →Bootstrap
嘗試去加載第三方實(shí)現(xiàn)類,發(fā)現(xiàn)不在自己的加載范圍內(nèi),無法加載 → 依賴于SPI的動(dòng)態(tài)服務(wù)發(fā)現(xiàn)機(jī)制,這些實(shí)現(xiàn)類會(huì)被交由線程上下文類加載器進(jìn)行加載(在前面講過,線程上下文加載器在Launcher
構(gòu)造函數(shù)被設(shè)置為了App
類加載器) → 通過App
系統(tǒng)類加載器加載第三方實(shí)現(xiàn)類,發(fā)現(xiàn)這些實(shí)現(xiàn)類在App
的加載范圍內(nèi),可以被加載,SPI接口的實(shí)現(xiàn)類加載完成.....
加載流程如上,很明顯的就可以感覺出來,線程上下文類加載器介入后,輕而易舉的打破了原有的雙親委派模型,同時(shí),也正是因?yàn)榫€程上下文類加載器的出現(xiàn),從而使得Java的類加載器機(jī)制更加靈活,方便。
5.2、線程上下文類加載器與SPI機(jī)制總結(jié)
簡單來說,Java提供了很多核心接口的定義,這些接口被稱為SPI接口,同時(shí)為了方便加載第三方的實(shí)現(xiàn)類,SPI提供了一種動(dòng)態(tài)的服務(wù)發(fā)現(xiàn)機(jī)制(約定),只要第三方在編寫實(shí)現(xiàn)類時(shí),在工程內(nèi)新建一個(gè)META-INF/services/
目錄并在該目錄下創(chuàng)建一個(gè)與服務(wù)接口名稱同名的文件,那么在程序啟動(dòng)的時(shí)候,就會(huì)根據(jù)約定去找到所有符合規(guī)范的實(shí)現(xiàn)類,然后交給線程上下文類加載器進(jìn)行加載處理。
六、JVM類加載子系統(tǒng)總結(jié)
至此,Java類加載機(jī)制的核心點(diǎn)就分析的七七八八了,但如果有對于類加載機(jī)制特別感興趣的小伙伴,也可以去看看Spring與Tomcat中的類加載器,但本次主要是對于JVM的知識(shí)進(jìn)行分析,對其他的源碼則不再進(jìn)行拓展了,后續(xù)的文章中可能會(huì)更新框架源碼分析相關(guān)的內(nèi)容。
總歸來說,Java類加載機(jī)制的核心點(diǎn)也是咱們開篇提到的那幾個(gè)重點(diǎn):類加載過程、類加載器、雙親委派模型、自定義類加載器以及線程上下文類加載器,掌握這幾部分即可,其他方面如果想深入探討的小伙伴可以自行研究。