大家都知道隅很,我們編寫的Java類經(jīng)過編譯器編譯后會(huì)生成class文件榨咐,class文件描述了類的各種信息稚补,最終都要加載到內(nèi)存中才能運(yùn)行使用楔绞,那虛擬機(jī)是如何加載這些class文件的呢蛇耀?加載又有哪些過程呢辩诞?是否程序一啟動(dòng)就把所有的類都加載到內(nèi)存中呢?下面我們就來討論這些問題纺涤。
上面說到Java類經(jīng)過編譯器編譯后會(huì)生成class文件译暂,這個(gè)說法在現(xiàn)在可能有些不準(zhǔn)確了抠忘,更準(zhǔn)確的說法應(yīng)該是“Java類經(jīng)過前端編譯器編譯后會(huì)生成class文件”,因?yàn)楝F(xiàn)在的Java會(huì)有一個(gè)JIT機(jī)制外永,JIT屬于后端編譯崎脉,JIT編譯器會(huì)在Java程序運(yùn)行期間將“熱點(diǎn)代碼”編譯成機(jī)器碼,以提高運(yùn)行效率伯顶。本文所提到的編譯都是前端編譯囚灼,不涉及后端編譯,關(guān)于后端編譯祭衩,會(huì)在下一篇文章中詳細(xì)討論灶体。
1 什么是類加載
編寫好Java代碼經(jīng)Java編譯器(Javac)編譯之后會(huì)生成class字節(jié)碼文件,這是我們從剛開始學(xué)習(xí)Java就知道的事掐暮。實(shí)際上蝎抽,這僅僅是Java程序運(yùn)行的第一步,JVM還必須在運(yùn)行時(shí)將字節(jié)碼載入到內(nèi)中路克,然后驗(yàn)證樟结、分析字節(jié)碼文件,并執(zhí)行相應(yīng)的指令衷戈,最后該class文件對(duì)應(yīng)的Java類才能被使用狭吼,這就是類加載機(jī)制。
那為什么會(huì)有這個(gè)類加載機(jī)制呢殖妇?學(xué)過C++的朋友應(yīng)該知道刁笙,C++程序要運(yùn)行大體有編譯和鏈接兩個(gè)過程,這樣的好處是運(yùn)行時(shí)效率非常高谦趣,不需要在做額外的操作疲吸,但大型的C++程序的編譯速度會(huì)慢得令人發(fā)指。Java就不這樣干前鹅,它會(huì)先編譯源代碼成字節(jié)碼摘悴,然后在運(yùn)行時(shí)動(dòng)態(tài)的將字節(jié)碼加載到內(nèi)存中,這樣的效果是大大降低了編譯速度和啟動(dòng)速度(我們發(fā)現(xiàn)即使大型的Java程序舰绘,編譯和啟動(dòng)過程都不會(huì)太慢)蹂喻,只有需要用到某個(gè)類的時(shí)候才會(huì)將其字節(jié)碼加載到內(nèi)存中,但運(yùn)行時(shí)效率就會(huì)受到影響(不過隨著JIT技術(shù)的成熟捂寿,這個(gè)方面的性能問題已經(jīng)得到了很大的改善)口四,這是Java程序運(yùn)行時(shí)性能不如C++程序的一方面原因。
2 類加載過程
上圖是Java類的生命周期秦陋,從加載到卸載蔓彩。我們主要關(guān)注的是加載、驗(yàn)證、準(zhǔn)備赤嚼、解析和初始化5個(gè)階段旷赖,使用和卸載暫不討論。這些階段都是交叉運(yùn)行的更卒,例如加載階段可能還沒完成等孵,驗(yàn)證就已經(jīng)開始了,這有點(diǎn)像流水線作業(yè)一樣逞壁。Java虛擬機(jī)沒有明確規(guī)定什么時(shí)候應(yīng)該開始加載一個(gè)類流济,但規(guī)定了什么時(shí)候應(yīng)該開始初始化一個(gè)類,而加載的開始必須要發(fā)生在初始化開始之前(但初始化開始并不就一定需要等待加載階段結(jié)束)腌闯。虛擬機(jī)規(guī)定了如下5種情況必須立即對(duì)類進(jìn)行初始化:
- 遇到new绳瘟、getstatic、putstatic和invokestatic這四條指令時(shí)姿骏,如果該類沒有進(jìn)行過初始化糖声,就必須先觸發(fā)其初始化操作。
- 使用java.util.reflect包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候分瘦,如果該類沒有進(jìn)行過初始化蘸泻,就必須先觸發(fā)其初始化操作。
- 當(dāng)初始化一個(gè)類時(shí)嘲玫,如果其父類沒有進(jìn)行過初始化悦施,就先觸發(fā)其父類的初始化。
- 當(dāng)虛擬機(jī)啟動(dòng)時(shí)去团,會(huì)先啟動(dòng)用戶指定的主類(包含main方法的類)抡诞。
- 當(dāng)使用動(dòng)態(tài)代理相關(guān)技術(shù)時(shí),如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果是REF_getStatic土陪、REF_pubSttic昼汗、REF_invokeStatic的方法句柄,并且這個(gè)方法所對(duì)應(yīng)的類沒有進(jìn)行過初始化鬼雀,那么必須先觸發(fā)其初始化顷窒。
“有且僅有”上述5種情況才會(huì)觸發(fā)初始化,這5中情況的行為被稱作“主動(dòng)引用”源哩,其他引用類的方式都不會(huì)觸發(fā)初始化鞋吉,被稱作“被動(dòng)引用”。只有根據(jù)這5種情況來判斷類是否初始化才是正確的励烦,根據(jù)其他的諸如“經(jīng)驗(yàn)法則”等會(huì)很容易出錯(cuò)谓着,所以正確理解5種情況所要表達(dá)的意義才是關(guān)鍵。
2.1 加載階段
加載階段崩侠,主要完成以下3個(gè)事情:
- 通過一個(gè)類的全限定類名來獲取該類對(duì)應(yīng)的二進(jìn)制字節(jié)流漆魔。
- 將這個(gè)字節(jié)流轉(zhuǎn)換成方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
- 在內(nèi)存中生成代表這個(gè)類的class對(duì)象却音,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)訪問的入口改抡。
這里的二進(jìn)制字節(jié)流并不一定就是class文件,只要是二進(jìn)制字節(jié)流就行系瓢,也沒有規(guī)定該字節(jié)流從哪獲取阿纤,所以其實(shí)獲取字節(jié)流的方式有很多,例如從壓縮包中獲取夷陋、網(wǎng)絡(luò)傳輸通道中獲取欠拾,運(yùn)行時(shí)生成(動(dòng)態(tài)代理等技術(shù))等。加載階段是類加載整個(gè)過程中唯一可以由開發(fā)人員掌控的骗绕,開發(fā)人員可以通過重寫Classloader的loadClass()方法來更改加載的方式藐窄,但最好要遵循雙親委派模型。
數(shù)組類和普通類的加載階段有一些差別酬土。數(shù)組類是由虛擬機(jī)直接創(chuàng)建的荆忍,而不是由類加載器創(chuàng)建的,但數(shù)組類和加載器仍然有密切的關(guān)系撤缴。如果數(shù)組的元素類型是引用類型刹枉,那么就根據(jù)普通類的加載規(guī)則去加載該類,數(shù)組類將與加載該類的加載器建立唯一關(guān)系標(biāo)識(shí)屈呕,如果數(shù)組元素類似引用類型微宝,那么數(shù)組類將與bootstrap加載器建立唯一關(guān)系標(biāo)識(shí)。
加載完成之后虎眨,會(huì)在方法區(qū)中生成一個(gè)代表該類的Class對(duì)象蟋软,class對(duì)象雖然是對(duì)象,但確實(shí)是存儲(chǔ)在方法區(qū)里专甩,這算是一個(gè)特例钟鸵,主要目的應(yīng)該是方便直接在方法區(qū)里訪問Class對(duì)象。
2.2 驗(yàn)證
驗(yàn)證和加載階段是交叉運(yùn)行的涤躲,即加載階段可能剛剛開始加載字節(jié)流棺耍,驗(yàn)證階段就開始對(duì)字節(jié)流進(jìn)行驗(yàn)證了,但驗(yàn)證開始時(shí)機(jī)仍然發(fā)生在加載開始之后种樱。
Java號(hào)稱是一門安全的語言蒙袍,所以驗(yàn)證階段就顯得尤為重要,在驗(yàn)證階段中嫩挤,虛擬機(jī)會(huì)驗(yàn)證字節(jié)碼是否符號(hào)虛擬機(jī)規(guī)范害幅,是否存在惡意的字節(jié)碼指令,邏輯是否符合Java語言規(guī)范等岂昭。如果驗(yàn)證失敗以现,虛擬機(jī)應(yīng)該拋出一個(gè)java.lang.VerifyError異常或者其子類并停止類加載過程。驗(yàn)證階段大致分為4個(gè)校驗(yàn)動(dòng)作:文件格式驗(yàn)證邑遏、元數(shù)據(jù)驗(yàn)證佣赖、字節(jié)碼驗(yàn)證、符號(hào)引用驗(yàn)證记盒。
2.2.1 文件格式驗(yàn)證
文件格式驗(yàn)證即驗(yàn)證字節(jié)流是否符合Class文件的格式規(guī)范憎蛤,例如開頭的CAFEBABE魔數(shù),版本號(hào)纪吮、常量池等各種信息的先后順序俩檬、有UTF-8要求的字符串是否滿足UTF-8字符編碼等等。這個(gè)階段的操作目標(biāo)是字節(jié)流碾盟,只有通過了這個(gè)階段的驗(yàn)證棚辽,并將字節(jié)流描述的類存儲(chǔ)到方法區(qū)中,才能進(jìn)行后面的驗(yàn)證冰肴,因?yàn)楹竺娴膸讉€(gè)驗(yàn)證動(dòng)作都是基于方法區(qū)的數(shù)據(jù)結(jié)構(gòu)來做的晚胡。
2.2.2 元數(shù)據(jù)驗(yàn)證
這個(gè)階段就是對(duì)字節(jié)碼描述的信息做語義分析,驗(yàn)證字節(jié)碼是否符合Java語言的規(guī)范嚼沿,例如這個(gè)類是否有父類估盘,這個(gè)類是否被設(shè)置成不可繼承的類,如果該類不是抽象類且實(shí)現(xiàn)了接口骡尽,是否實(shí)現(xiàn)了接口的抽象方法等等遣妥。這里還要說一下,Java語言規(guī)范和Java虛擬機(jī)規(guī)范是兩碼事攀细,不能一概而論箫踩。
2.2.3 字節(jié)碼驗(yàn)證
這個(gè)階段主要進(jìn)行的是用數(shù)據(jù)流和控制流分析程序語義是否合法,保證被校驗(yàn)的類不會(huì)做出危害虛擬機(jī)的事情谭贪。該階段是很復(fù)雜的境钟,也是比較耗時(shí)的,所以后期的Java虛擬機(jī)對(duì)整個(gè)步驟做了一些優(yōu)化俭识,使用“StackMapTable”屬性來保存本地變量表和操作數(shù)等信息慨削,當(dāng)需要進(jìn)行字節(jié)碼驗(yàn)證的時(shí)候,就直接驗(yàn)證“StackMapTable”里的信息即可套媚,大大減少了驗(yàn)證時(shí)間缚态。
2.2.4 符號(hào)引用驗(yàn)證
這個(gè)階段會(huì)嚴(yán)重符號(hào)引用是否正確,符號(hào)引用是否正確的判斷依據(jù)主要有以下幾個(gè):
是否能通過描述符號(hào)引用的全限定類名字符串找到對(duì)應(yīng)的類堤瘤。
在指定的類中是否存在符合方法的字段描述符以及簡(jiǎn)單名稱所描述的方法和字段玫芦。
-
符號(hào)引用中的類、字段本辐、方法的訪問性是否可以被當(dāng)前類訪問桥帆。
.......
只有完成了符號(hào)引用驗(yàn)證医增,后續(xù)在解析階段將符號(hào)引用轉(zhuǎn)換成直接引用的時(shí)候才可能成功,如果驗(yàn)證失敗老虫,將會(huì)拋出java.lang.NoSuchMethodError调窍、java.lang.NoSuchFieldError等異常。
對(duì)于類加載機(jī)制來說张遭,驗(yàn)證階段雖然是非常重要的,但并不是必須的地梨,如果要運(yùn)行的代碼已經(jīng)經(jīng)歷過反復(fù)驗(yàn)證和使用菊卷,那么就可以省略掉驗(yàn)證這個(gè)階段,從而降低類加載的時(shí)間宝剖。
2.3 準(zhǔn)備
準(zhǔn)備階段是為類變量分配內(nèi)存并初始化的階段洁闰,這些變量所使用內(nèi)存都是方法區(qū)內(nèi)存,需要注意的是万细,類變量和實(shí)例變量是不同的扑眉,準(zhǔn)備階段僅包括類變量(static修飾的)的內(nèi)存分配和初始化。初始化是給變量賦予對(duì)應(yīng)類型的“零值”赖钞,這里的“零值”并不是特點(diǎn)的數(shù)字0腰素,對(duì)于數(shù)字類型來說確實(shí)是0或者0.0,對(duì)于布爾型變量來說是false雪营,引用類型是null等弓千,下面這個(gè)表格給出了各種類型的“零”值:
即使用戶在聲明的時(shí)候并同時(shí)賦值,也不會(huì)馬上按照程序員的意愿進(jìn)行操作献起,如下所示:
public class Main {
private static int a = 123;
}
這里a在僅僅會(huì)被賦值成0洋访,而不是123。但有一個(gè)例外谴餐,就是常量姻政!常量會(huì)直接根據(jù)程序員的意愿進(jìn)行操作:
public class Main {
private static final int a = 123;
}
a被final修飾了,所以他是一個(gè)常量岂嗓,在準(zhǔn)備階段汁展,虛擬機(jī)會(huì)根據(jù)常量值做賦值操作,即準(zhǔn)備階段完成后厌殉,a的值是123而不是0善镰。
2.4 解析
解析過程的作用是將符號(hào)引用轉(zhuǎn)換成虛擬機(jī)可以直接使用的直接引用。在之前的文章中年枕,有不少地方提到過符號(hào)引用炫欺,但一直沒有詳細(xì)解釋什么是符號(hào)引用,在此就詳細(xì)介紹一下吧:
- 符號(hào)引用熏兄。符號(hào)引用可以是任何形式的字面值常量品洛,只要能在使用時(shí)無歧義的定位到目標(biāo)即可树姨。在HotSpot虛擬機(jī)中,是以字符串的形式存在的桥状,而且往往是一組字符串帽揪。這組字符串所代表的可能是某個(gè)類、某個(gè)接口等辅斟,無論代表的是什么转晰,只要能唯一的定位到目標(biāo),那就是一個(gè)正確的符號(hào)引用士飒。關(guān)于符號(hào)引用的更多查邢,推薦看看知乎上這個(gè)問題:JVM里的符號(hào)引用如何存儲(chǔ)。
- 直接引用酵幕。直接引用可以是指向目標(biāo)的指針扰藕、相對(duì)偏移量或者一個(gè)能間接定位到目標(biāo)的句柄等,直接引用和虛擬機(jī)的內(nèi)存布局是有關(guān)的芳撒,同一個(gè)符號(hào)引用在不同的虛擬機(jī)里翻譯處理的直接引用往往不相同邓深,如果成功將符號(hào)引用轉(zhuǎn)換成直接引用了,那么直接引用的目標(biāo)肯定是已經(jīng)存在于內(nèi)存中的笔刹。
解析階段的對(duì)象不僅僅是類芥备,還包括接口、方法舌菜、字段等门躯。因?yàn)橐L問一個(gè)接口或者方法、字段都需要有一個(gè)直接引用酷师,而直接引用又是由符號(hào)引用轉(zhuǎn)換而成的讶凉。關(guān)于解析更加詳細(xì)的內(nèi)容建議細(xì)節(jié)看看《深入理解Java虛擬機(jī)》的7.3.4節(jié)內(nèi)容。
2.5 初始化
初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程山孔。需要注意的是這里的<clinit>()方法不包括實(shí)例構(gòu)造器懂讯,實(shí)例構(gòu)造器屬于<init>()方法,換句話說台颠,這里不會(huì)執(zhí)行實(shí)例構(gòu)造器或者初始化代碼塊里的內(nèi)容褐望。這是因?yàn)檫@里的初始化階段還屬于類加載的過程,沒有涉及到實(shí)例化的過程串前,實(shí)例構(gòu)造器或者初始化代碼塊的代碼會(huì)在類被實(shí)例化成對(duì)象的時(shí)候執(zhí)行瘫里。
<clinit>()方法由靜態(tài)變量的賦值語句和static代碼塊里的語句構(gòu)成,出現(xiàn)在前的語句在合并后仍然出現(xiàn)在前荡碾,即順序保持源碼中的順序谨读。有一個(gè)比較奇怪的現(xiàn)象,在我們編寫源碼的時(shí)候坛吁,static塊里能對(duì)聲明在后面的變量做賦值操作劳殖,只是不能訪問铐尚。
<clinit>()方法不需要顯式的調(diào)用父類的<clinit>()方法(實(shí)例構(gòu)造器需要顯示的調(diào)用,只是大多數(shù)時(shí)候哆姻,編譯器會(huì)幫我們?cè)诘谝恍刑砩狭耍┬觯摂M機(jī)會(huì)保證在調(diào)用子類的<clinit>()方法之前調(diào)用父類的<clinit>()方法∶В基于這個(gè)機(jī)制爹脾,父類的靜態(tài)變量的賦值操作優(yōu)先于子類的靜態(tài)變量賦值。
<clinit>()方法并不是必須的箕昭,如果一個(gè)類沒有任何靜態(tài)變量和靜態(tài)塊灵妨,那么虛擬機(jī)就不會(huì)為該類生成<clinit>()方法。還有就是雖然接口不能定義靜態(tài)塊盟广,但可以定義靜態(tài)變量,所以接口也是可能有<clinit>()瓮钥,但和類的<clinit>()不同筋量,接口執(zhí)行<clinit>()方法之前不需要執(zhí)行父接口的<clinit>()方法,只有在父接口中定義的變量被使用時(shí)碉熄,才會(huì)調(diào)用父接口的<clinit>方法桨武。
虛擬機(jī)還會(huì)保證<clinit>方法是線程安全的,即多個(gè)線程同時(shí)要去執(zhí)行<clinit>()方法也僅僅有一個(gè)方法能真正執(zhí)行锈津,其他線程會(huì)被阻塞呀酸,當(dāng)能執(zhí)行<clinit>()方法的線程執(zhí)行完畢之后,其他線程被喚醒琼梆,但不會(huì)再去嘗試執(zhí)行<clinit>方法性誉,這避免了重復(fù)執(zhí)行<clinit>()。我們可以寫一些代碼嘗試一下:
public class ClinitTest {
static class Test {
private static int a = 32;
static {
a = 42;
System.out.println(Thread.currentThread().getName() + "execute static");
}
}
public static void main(String[] args) throws InterruptedException {
Runnable r = () -> {
System.out.println(Thread.currentThread().getName() + " started");
Test test = new Test();
System.out.println(Thread.currentThread().getName() + "end");
};
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
結(jié)果如下所示:
Thread-0 started
Thread-1 started
Thread-1execute static
Thread-1end
Thread-0end
可見茎杂,<clinit>()方法只被執(zhí)行了一次错览,符合我們上面說到的規(guī)則。
3 類加載器
類加載器是這么一個(gè)東西:可以通過一個(gè)類的全限定類名獲取描述該類的二進(jìn)制字節(jié)流的代碼模塊煌往。有了上面的分析倾哺,我們知道這其實(shí)是“加載”階段的一個(gè)步驟,虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)之所以單獨(dú)將其抽離出來刽脖,是為了方便應(yīng)用程序自己決定如何獲取需要的字節(jié)流羞海。
我們可以通過重寫ClassLoader的loadClass()方法來改變加載類的方式,如下代碼所示:
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassLoader classLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
try {
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] bytes = new byte[is.available()];
is.read(bytes);
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw new ClassNotFoundException();
}
}
};
Class<?> clz = classLoader.loadClass("top.yeonon.ch11.ClassLoaderTest");
System.out.println(clz);
System.out.println(clz.newInstance() instanceof ClassLoaderTest);
}
}
代碼重寫了loadClass方法曲管,只是簡(jiǎn)單的通過class文件來獲取却邓。其中最后一行代碼的返回結(jié)果是false,為什么呢院水?因?yàn)槲覀冇米约褐貙懥薼oadClass()方法的classLoader來加載類申尤,這里獲取到的類和虛擬機(jī)加載類是不一樣的癌幕,即使它們是同一個(gè)類。那為什么會(huì)不一樣呢昧穿?因?yàn)轭惣虞d器不一樣勺远,現(xiàn)在虛擬機(jī)中有兩個(gè)ClassLoaderTest類,一個(gè)是由系統(tǒng)的類加載器加載的(更準(zhǔn)確的應(yīng)該是ApplicationClassLoader)时鸵,一個(gè)是由我們自己實(shí)現(xiàn)的classloader加載胶逢,虛擬機(jī)判斷兩個(gè)類是否是同一個(gè)類不僅僅是通過他們的全限定類名來判斷,還通過加載他們的類加載器是否一樣來判斷饰潜,只有滿足上述兩個(gè)條件初坠,虛擬機(jī)才會(huì)認(rèn)為兩個(gè)類是相同的。
既然講到了ApplicationClassLoader彭雾,接下來就討論一下雙親委派模型碟刺。
3.1 雙親委派模型
在JDK8及以下的版本中(JDK9之后有不小的改動(dòng)),默認(rèn)的有三種類加載器:
- BootStrap ClassLoader(引導(dǎo)類加載器)
- Extension ClassLoader(擴(kuò)展類加載器)
- Application ClassLoader(應(yīng)用類加載器)
引導(dǎo)類加載器負(fù)責(zé)加載$JAVA_HOME/lib下的薯酝,或者被-Xbootclasspath參數(shù)指定的路徑下的半沽,并且是虛擬機(jī)識(shí)別的(有些類即使在上述兩個(gè)路徑下,也不會(huì)被加載)類吴菠。這個(gè)類加載器是由C++實(shí)現(xiàn)的(HotSpot虛擬機(jī))者填,所以使用Java代碼無法獲取,只會(huì)返回null做葵,當(dāng)我們需要將類加載委托給它時(shí)占哟,用null代替接口。
擴(kuò)展類加載器負(fù)責(zé)加載$JAVA_HOME/lib/ext目錄酿矢,或者被java.ext.dirs系統(tǒng)變量指定的路徑下的類榨乎。這個(gè)類加載器是由Java語言實(shí)現(xiàn)的,用戶可以直接使用該類加載器瘫筐。
應(yīng)用類加載器負(fù)責(zé)加載classpath中指定的路徑中的類谬哀,一般我們編寫的Java類都是由這個(gè)類加載的。
有了上述三個(gè)概念严肪,我們就可以看看雙親委派模型的定義了:如果一個(gè)類加載器收到了類加載請(qǐng)求史煎,它不會(huì)自己馬上去嘗試加載這個(gè)類,而是將這個(gè)請(qǐng)求委托給父類加載器完成驳糯,如果父類加載器上面還有父類加載器篇梭,那么會(huì)繼續(xù)將委托向上提交,直到引導(dǎo)類加載器酝枢,如果引導(dǎo)類加載器無法加載這個(gè)類恬偷,就會(huì)將請(qǐng)求往下傳,只要中途有一個(gè)類加載器加載成功了帘睦,就不會(huì)繼續(xù)往下走了袍患。
為了理解這個(gè)過程坦康,舉個(gè)例子。假設(shè)我們現(xiàn)在編寫了一個(gè)top.yeonon.Test類诡延,當(dāng)需要加載這個(gè)類的時(shí)候滞欠,如果沒有其他類加載器,默認(rèn)就先將請(qǐng)求發(fā)送到Application ClassLoader肆良,Application ClassLoader有父類加載器Extension ClassLoader筛璧,所以它就將請(qǐng)求發(fā)送到Extension ClassLoader,Extension ClassLoader也同理惹恃,最終請(qǐng)求達(dá)到最頂層的BootStrap ClassLoader夭谤,BootStrap ClassLoader發(fā)現(xiàn)top.yeonon.Test這個(gè)類自己不能加載,然后將請(qǐng)求原路返回巫糙,到Extension ClassLoader的時(shí)候朗儒,Extension ClassLoader發(fā)現(xiàn)自己也不能加載,然后再回到Application ClassLoader参淹,這時(shí)候沒地方去了醉锄,Application ClassLoader才會(huì)嘗試去加載該類,如果加載成功(該類確實(shí)在classpath路徑下)承二,那么就完成了類加載榆鼠,如果加載失敗纲爸,就會(huì)拋出異常亥鸠。下面是雙親委派模型的示意圖:
那為什么Java要搞這么一套雙親委派模型呢?為了保證安全识啦,試想一下负蚊,假設(shè)我們現(xiàn)在編寫了一個(gè)java.lang.String類,在這類里加入了一些惡意代碼颓哮,如果沒有雙親委派模型家妆,這個(gè)類就會(huì)直接被Application ClassLoader加載,當(dāng)用戶使用String類的時(shí)候冕茅,就會(huì)用到這個(gè)含有惡意代碼的類伤极,從而造成應(yīng)用程序崩潰或者重要信息泄露。
4 小結(jié)
本文介紹了什么是類加載姨伤、類加載過程已經(jīng)類加載器和雙親委派模型哨坪,類加載是一個(gè)比較獨(dú)特的特性,這個(gè)機(jī)制使得Java程序更加安全乍楚、高效当编,理解類加載過程也有助于解決各種由于類加載導(dǎo)致的問題。
5 參考資料
《深入理解Java虛擬機(jī)》