相關(guān)好文:面試官對(duì)于JVM類(lèi)加載機(jī)制的猛烈炮火掰担,你能頂住嗎悍抑?
https://juejin.im/post/5d1d4f8ef265da1b60291fec
類(lèi)是在運(yùn)行期間第一次使用時(shí)動(dòng)態(tài)加載的希坚,而不是一次性加載。因?yàn)槿绻淮涡约虞d崇堰,那么會(huì)占用很多的內(nèi)存虚青。
類(lèi)加載的機(jī)制的層次結(jié)構(gòu)
每個(gè)編寫(xiě)的”.java”拓展名類(lèi)文件都存儲(chǔ)著需要執(zhí)行的程序邏輯,這些”.java”文件經(jīng)過(guò)Java編譯器編譯成拓展名為”.class”的文件苞也,”.class”文件中保存著Java代碼經(jīng)轉(zhuǎn)換后的虛擬機(jī)指令洛勉,當(dāng)需要使用某個(gè)類(lèi)時(shí),虛擬機(jī)將會(huì)加載它的”.class”文件如迟,并創(chuàng)建對(duì)應(yīng)的class對(duì)象收毫,將class文件加載到虛擬機(jī)的內(nèi)存攻走,這個(gè)過(guò)程稱(chēng)為類(lèi)加載,這里我們需要了解一下類(lèi)加載的過(guò)程此再,如下:
加載:類(lèi)加載過(guò)程的一個(gè)階段昔搂,通過(guò)一個(gè)類(lèi)的完全限定查找此類(lèi)字節(jié)碼文件,并利用字節(jié)碼文件創(chuàng)建一個(gè)Class對(duì)象
1引润、通過(guò)一個(gè)類(lèi)的全限定名獲取描述此類(lèi)的二進(jìn)制字節(jié)流巩趁;
2痒玩、將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)保存為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)淳附;
3、在java堆中生成一個(gè)代表這個(gè)類(lèi)的java.lang.Class對(duì)象蠢古,作為訪(fǎng)問(wèn)方法區(qū)的入口奴曙;
驗(yàn)證:目的在于確保Class文件的字節(jié)流中包含信息符合當(dāng)前虛擬機(jī)要求,不會(huì)危害虛擬機(jī)自身安全草讶。主要包括四種驗(yàn)證洽糟,文件格式驗(yàn)證,元數(shù)據(jù)驗(yàn)證堕战,字節(jié)碼驗(yàn)證坤溃,符號(hào)引用驗(yàn)證。
準(zhǔn)備:為類(lèi)變量(即static修飾的字段變量)分配內(nèi)存并且設(shè)置該類(lèi)變量的初始值嘱丢,使用的是方法區(qū)的內(nèi)存薪介。
實(shí)例變量不會(huì)在這階段分配內(nèi)存,它將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在Java堆中越驻。
注意汁政,實(shí)例化不是類(lèi)加載的一個(gè)過(guò)程,類(lèi)加載發(fā)生在所有實(shí)例化操作之前缀旁,并且類(lèi)加載只進(jìn)行一次记劈,實(shí)例化可以進(jìn)行多次。
初始值一般為 0 值并巍,例如下面的類(lèi)變量 value 被初始化為 0 而不是 123目木。
public static int value = 123;
如果類(lèi)變量是常量,那么會(huì)按照表達(dá)式來(lái)進(jìn)行初始化懊渡,而不是賦值為 0刽射。
public static final int value = 123;
解析:主要將常量池中的【符號(hào)引用】替換為【直接引用】的過(guò)程。符號(hào)引用就是一組符號(hào)來(lái)描述目標(biāo)距贷,可以是任何字面量柄冲,而直接引用就是直接指向目標(biāo)的指針、相對(duì)偏移量或一個(gè)間接定位到目標(biāo)的句柄忠蝗。其中解析過(guò)程在某些情況下可以在初始化階段之后再開(kāi)始现横,這是為了支持 Java 的動(dòng)態(tài)綁定。
解析的時(shí)候class已經(jīng)被加載到方法區(qū)的內(nèi)存中,因此要把符號(hào)引用轉(zhuǎn)化為直接引用戒祠,也就是能直接找到該類(lèi)實(shí)際內(nèi)存地址的引用骇两。
分為類(lèi)或接口的解析,字段解析姜盈,類(lèi)方法解析低千,接口方法解析。
初始化:類(lèi)加載最后階段馏颂,若該類(lèi)具有超類(lèi)示血,則對(duì)其進(jìn)行初始化,執(zhí)行靜態(tài)初始化器和靜態(tài)初始化成員變量(如前面只初始化了默認(rèn)值的static變量將會(huì)在這個(gè)階段賦值救拉,成員變量也將被初始化)难审。
初始化階段是執(zhí)行類(lèi)構(gòu)造器<clinit>方法的過(guò)程,<clinit>方法由類(lèi)變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊按照在源文件出現(xiàn)的順序合并而成亿絮,該合并操作由編譯器完成告喊。
1)<clinit>方法對(duì)于類(lèi)或接口不是必須的,如果一個(gè)類(lèi)中沒(méi)有靜態(tài)代碼塊派昧,也沒(méi)有靜態(tài)變量的賦值操作黔姜,那么編譯器不會(huì)生成<clinit>;
2)<clinit>方法與實(shí)例構(gòu)造器不同蒂萎,不需要顯式的調(diào)用父類(lèi)的<clinit>方法秆吵,虛擬機(jī)會(huì)保證父類(lèi)的<clinit>優(yōu)先執(zhí)行;
3)為了防止多次執(zhí)行<clinit>岖是,虛擬機(jī)會(huì)確保<clinit>方法在多線(xiàn)程環(huán)境下被正確的加鎖同步執(zhí)行帮毁,如果有多個(gè)線(xiàn)程同時(shí)初始化一個(gè)類(lèi),那么只有一個(gè)線(xiàn)程能夠執(zhí)行<clinit>方法豺撑,其它線(xiàn)程進(jìn)行阻塞等待烈疚,直到<clinit>執(zhí)行完成。
4)注意:執(zhí)行接口的<clinit>方法不需要先執(zhí)行父接口的<clinit>聪轿,只有使用父接口中定義的變量時(shí)爷肝,才會(huì)執(zhí)行。
對(duì)象的初始化順序:
????????????????? 基類(lèi)的static域加載
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?|
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?|
? ? ? ? ? ? ? ? ?子類(lèi)的static域加載?????????????????? ?????
??????????????????????? ????? |
??????????????????????? ????? |
基類(lèi)的成員變量初始化(基本類(lèi)型初始化為默認(rèn)值陆错,對(duì)象引用設(shè)為null灯抛,若有初值則初始化為初值)???????????
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?|
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?|
基類(lèi)的構(gòu)造器加載(若在構(gòu)造器中調(diào)用的方法在子類(lèi)中被覆蓋過(guò),則調(diào)用覆蓋后的方法)
??????????????????????? ????? |
??????????????????????? ????? |
???????????? 子類(lèi)的成員變量初始化???????????????????????????? ?????
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?|
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?|
? ? ? ? ? ? ? ? ?子類(lèi)的構(gòu)造器加載
類(lèi)初始化場(chǎng)景:
1音瓷,主動(dòng)引用
虛擬機(jī)中嚴(yán)格規(guī)定了有且只有5種情況必須對(duì)類(lèi)進(jìn)行初始化对嚼。
* 執(zhí)行new、getstatic绳慎、putstatic和invokestatic指令纵竖;
* 使用reflect對(duì)類(lèi)進(jìn)行反射調(diào)用漠烧;
* 初始化一個(gè)類(lèi)的時(shí)候,父類(lèi)還沒(méi)有初始化靡砌,會(huì)事先初始化父類(lèi)已脓;
* 啟動(dòng)虛擬機(jī)時(shí),需要初始化包含main方法的類(lèi)通殃;
* 在JDK1.7中度液,如果java.lang.invoke.MethodHandler實(shí)例最后的解析結(jié)果REF_getStatic、REF_putStatic画舌、REF_invokeStatic的方法句柄堕担,并且這個(gè)方法句柄對(duì)應(yīng)的類(lèi)沒(méi)有進(jìn)行初始化;
2骗炉,被動(dòng)引用
以上 5 種場(chǎng)景中的行為稱(chēng)為對(duì)一個(gè)類(lèi)進(jìn)行主動(dòng)引用照宝。除此之外,所有引用類(lèi)的方式都不會(huì)觸發(fā)初始化句葵,稱(chēng)為被動(dòng)引用。被動(dòng)引用的常見(jiàn)例子包括:
以下幾種情況兢仰,不會(huì)觸發(fā)類(lèi)初始化
1)通過(guò)子類(lèi)引用父類(lèi)的靜態(tài)字段乍丈,只會(huì)觸發(fā)父類(lèi)的初始化,而不會(huì)觸發(fā)子類(lèi)的初始化把将。
class Parent {
??? static int a = 100;
??? static {
???????System.out.println("parent init轻专!");
? ??}
}
class Child extends Parent {
??? static {
???????System.out.println("child init!");
??? }
}
public class Init{?
??? public static voidmain(String[] args){?
???????System.out.println(Child.a);?
??? }?
}?
輸出結(jié)果為:
parent init察蹲!
100
2)定義對(duì)象數(shù)組请垛,即通過(guò)數(shù)組定義來(lái)引用類(lèi),不會(huì)觸發(fā)該類(lèi)的初始化洽议。
public class Init{?
??? public static voidmain(String[] args){?
??????? Parent[] parents =new Parent[10];
??? }?
}?
無(wú)輸出宗收,說(shuō)明沒(méi)有觸發(fā)類(lèi)Parent的初始化,但是這段代碼做了什么亚兄?先看看生成的字節(jié)碼指令
anewarray指令為新數(shù)組分配空間混稽,并觸發(fā)[Lcom.ctrip.ttd.whywhy.Parent類(lèi)的初始化,這個(gè)類(lèi)由虛擬機(jī)自動(dòng)生成审胚。
3)常量在編譯期間會(huì)存入調(diào)用類(lèi)的常量池中匈勋,本質(zhì)上并沒(méi)有直接引用定義常量的類(lèi),不會(huì)觸發(fā)定義常量所在的類(lèi)膳叨。
class Const {
??? static final int A =100;
??? static {
???????System.out.println("Const init");
??? }
}
public class Init{?
??? public static voidmain(String[] args){?
??????? System.out.println(Const.A);?
??? }?
}?
輸出:
100
說(shuō)明沒(méi)有觸發(fā)類(lèi)Const的初始化洽洁,在編譯階段,Const類(lèi)中常量A的值100存儲(chǔ)到Init類(lèi)的常量池中菲嘴,這兩個(gè)類(lèi)在編譯成class文件之后就沒(méi)有聯(lián)系了饿自。
4)通過(guò)類(lèi)名獲取Class對(duì)象碎浇,不會(huì)觸發(fā)類(lèi)的初始化。
public class test {
?? public static voidmain(String[] args) throws ClassNotFoundException {
??????? Class c_dog =Dog.class;
??????? Class clazz =Class.forName("zzzzzz.Cat");
??? }
}
class Cat {
??? private String name;
??? private int age;
??? static {
???????System.out.println("Cat is load");
??? }
}
class Dog {
??? private String name;
?? ?private int age;
??? static {
???????System.out.println("Dog is load");
??? }
}
執(zhí)行結(jié)果:Cat is load璃俗,所以通過(guò)Dog.class并不會(huì)觸發(fā)Dog類(lèi)的初始化動(dòng)作奴璃。
5)通過(guò)Class.forName加載指定類(lèi)時(shí),如果指定參數(shù)initialize為false時(shí)城豁,也不會(huì)觸發(fā)類(lèi)初始化苟穆,其實(shí)這個(gè)參數(shù)是告訴虛擬機(jī),是否要對(duì)類(lèi)進(jìn)行初始化唱星。
public class test {
?? public static voidmain(String[] args) throws ClassNotFoundException {
??????? Class clazz =Class.forName("zzzzzz.Cat", false, Cat.class.getClassLoader());
??? }
}
class Cat {
??? private String name;
??? private int age;
??? static {
??????? System.out.println("Catis load");
??? }
}
6)通過(guò)ClassLoader默認(rèn)的loadClass方法雳旅,也不會(huì)觸發(fā)初始化動(dòng)作
new ClassLoader(){}.loadClass("zzzzzz.Cat");
JVM類(lèi)加載器
這便是類(lèi)加載的5個(gè)過(guò)程,而類(lèi)加載器的任務(wù)是根據(jù)一個(gè)類(lèi)的全限定名來(lái)讀取此類(lèi)的二進(jìn)制字節(jié)流到JVM中间聊,然后轉(zhuǎn)換為一個(gè)與目標(biāo)類(lèi)對(duì)應(yīng)的java.lang.Class對(duì)象實(shí)例攒盈,在虛擬機(jī)提供了3種類(lèi)加載器,啟動(dòng)(Bootstrap)類(lèi)加載器哎榴、擴(kuò)展(Extension)類(lèi)加載器型豁、系統(tǒng)(System)類(lèi)加載器(也稱(chēng)應(yīng)用類(lèi)加載器),下面分別介紹:
啟動(dòng)(Bootstrap)類(lèi)加載器
啟動(dòng)類(lèi)加載器主要加載的是JVM自身需要的類(lèi)尚蝌,這個(gè)類(lèi)加載使用C++語(yǔ)言實(shí)現(xiàn)的迎变,是虛擬機(jī)自身的一部分,它負(fù)責(zé)將 <JAVA_HOME>/lib路徑下的核心類(lèi)庫(kù)或-Xbootclasspath參數(shù)指定的路徑下的jar包加載到內(nèi)存中飘言,注意由于虛擬機(jī)是按照文件名識(shí)別加載jar包的衣形,如rt.jar,如果文件名不被虛擬機(jī)識(shí)別姿鸿,即使把jar包丟到lib目錄下也是沒(méi)有作用的(出于安全考慮谆吴,Bootstrap啟動(dòng)類(lèi)加載器只加載包名為java、javax苛预、sun等開(kāi)頭的類(lèi))句狼。
擴(kuò)展(Extension)類(lèi)加載器
擴(kuò)展類(lèi)加載器是指Sun公司實(shí)現(xiàn)的sun.misc.Launcher$ExtClassLoader類(lèi),由Java語(yǔ)言實(shí)現(xiàn)的碟渺,是Launcher的靜態(tài)內(nèi)部類(lèi)鲜锚,它負(fù)責(zé)加載<JAVA_HOME>/lib/ext目錄下或者由系統(tǒng)變量-Djava.ext.dir指定位路徑中的類(lèi)庫(kù),開(kāi)發(fā)者可以直接使用標(biāo)準(zhǔn)擴(kuò)展類(lèi)加載器苫拍。
應(yīng)用程序類(lèi)(Application)類(lèi)加載器
也稱(chēng)應(yīng)用程序加載器是指Sun公司實(shí)現(xiàn)的sun.misc.Launcher$AppClassLoader芜繁。它負(fù)責(zé)加載系統(tǒng)類(lèi)路徑j(luò)ava -classpath或-D java.class.path 指定路徑下的類(lèi)庫(kù),也就是我們經(jīng)常用到的classpath路徑绒极,開(kāi)發(fā)者可以直接使用系統(tǒng)類(lèi)加載器骏令,一般情況下該類(lèi)加載是程序中默認(rèn)的類(lèi)加載器,通過(guò)ClassLoader#getSystemClassLoader()方法可以獲取到該類(lèi)加載器垄提。
自定義(User)類(lèi)加載器
JVM提供的類(lèi)加載器只能加載指定目錄的類(lèi)(jar和class)榔袋,如果我們想從其他地方甚至網(wǎng)絡(luò)上獲取class文件周拐,就需要自定義類(lèi)加載器來(lái)實(shí)現(xiàn),自定義類(lèi)加載器主要都是通過(guò)繼承ClassLoader或者它的子類(lèi)來(lái)實(shí)現(xiàn)凰兑,但無(wú)論是通過(guò)繼承ClassLoader還是它的子類(lèi)妥粟,最終自定義類(lèi)加載器的父加載器都是應(yīng)用程序類(lèi)加載器,因?yàn)椴还苷{(diào)用哪個(gè)父類(lèi)加載器吏够,創(chuàng)建的對(duì)象都必須最終調(diào)用java.lang.ClassLoader.getSystemClassLoader()作為父加載器勾给,getSystemClassLoader()方法的返回值是sun.misc.Launcher.AppClassLoader即應(yīng)用程序類(lèi)加載器。
??????? 在Java的日常應(yīng)用程序開(kāi)發(fā)中锅知,類(lèi)的加載幾乎是由上述3種類(lèi)加載器相互配合執(zhí)行的播急,在必要時(shí),我們還可以自定義類(lèi)加載器售睹,需要注意的是桩警,Java虛擬機(jī)對(duì)class文件采用的是按需加載的方式,也就是說(shuō)當(dāng)需要使用該類(lèi)時(shí)才會(huì)將它的class文件加載到內(nèi)存生成class對(duì)象昌妹,而且加載某個(gè)類(lèi)的class文件時(shí)捶枢,Java虛擬機(jī)采用的是雙親委派模式即把請(qǐng)求交由父類(lèi)處理,它一種任務(wù)委派模式捺宗,下面我們進(jìn)一步了解它柱蟀。
雙親委派模式工作原理
雙親委派模式要求除了頂層的啟動(dòng)類(lèi)加載器外,其余的類(lèi)加載器都應(yīng)當(dāng)有自己的父類(lèi)加載器蚜厉,請(qǐng)注意雙親委派模式中的父子關(guān)系并非通常所說(shuō)的類(lèi)繼承關(guān)系,而是采用組合關(guān)系來(lái)復(fù)用父類(lèi)加載器的相關(guān)代碼畜眨,類(lèi)加載器間的關(guān)系如下:
工作過(guò)程:
一個(gè)類(lèi)加載器首先將類(lèi)加載請(qǐng)求傳送到父類(lèi)加載器昼牛,只有當(dāng)父類(lèi)加載器無(wú)法完成類(lèi)加載請(qǐng)求時(shí)才嘗試加載。
雙親委派模式是在Java 1.2后引入的康聂,其工作原理的是贰健,如果一個(gè)類(lèi)加載器收到了類(lèi)加載請(qǐng)求,它并不會(huì)自己先去加載恬汁,而是把這個(gè)請(qǐng)求委托給父類(lèi)的加載器去執(zhí)行伶椿,如果父類(lèi)加載器還存在其父類(lèi)加載器,則進(jìn)一步向上委托氓侧,依次遞歸脊另,請(qǐng)求最終將到達(dá)頂層的啟動(dòng)類(lèi)加載器,如果父類(lèi)加載器可以完成類(lèi)加載任務(wù)约巷,就成功返回偎痛,倘若父類(lèi)加載器無(wú)法完成此加載任務(wù),子加載器才會(huì)嘗試自己去加載独郎,這就是雙親委派模式踩麦,即每個(gè)兒子都很懶枚赡,每次有活就丟給父親去干,直到父親說(shuō)這件事我也干不了時(shí)谓谦,兒子自己想辦法去完成贫橙,這不就是傳說(shuō)中的實(shí)力坑爹啊反粥?那么采用這種模式有啥用呢卢肃?
//雙親委派模型的工作過(guò)程源碼
protected synchronized Class loadClass(String name,boolean resolve)
throws ClassNotFoundException
{
??? // First, check if theclass has already been loaded
??? Class c =findLoadedClass(name);
??? if (c == null) {
??????? try {
??????????? if (parent !=null) {
??????????????? c =parent.loadClass(name, false);
??????????? } else {
??????????????? c =findBootstrapClassOrNull(name);
??????????? }
??????? } catch(ClassNotFoundException e) {
??????????? //ClassNotFoundException thrown if class not found
??????????? // from thenon-null parent class loader
??????????? //父類(lèi)加載器無(wú)法完成類(lèi)加載請(qǐng)求
??????? }
??????? if (c == null) {
??????????? // If still notfound, then invoke findClass in order to find the class
??????????? //子加載器進(jìn)行類(lèi)加載
??????????? c =findClass(name);
??????? }
??? }
??? if (resolve) {
??????? //判斷是否需要鏈接過(guò)程,參數(shù)傳入
??????? resolveClass(c);
??? }
??? return c;
}
雙親委派模型的工作過(guò)程如下:
(1)當(dāng)前類(lèi)加載器從自己已經(jīng)加載的類(lèi)中查詢(xún)是否此類(lèi)已經(jīng)加載星压,如果已經(jīng)加載則直接返回原來(lái)已經(jīng)加載的類(lèi)践剂。
(2)如果沒(méi)有找到,就去委托父類(lèi)加載器去加載(如代碼c = parent.loadClass(name, false)所示)娜膘。父類(lèi)加載器也會(huì)采用同樣的策略逊脯,查看自己已經(jīng)加載過(guò)的類(lèi)中是否包含這個(gè)類(lèi),有就返回竣贪,沒(méi)有就委托父類(lèi)的父類(lèi)去加載军洼,一直到啟動(dòng)類(lèi)加載器。因?yàn)槿绻讣虞d器為空了演怎,就代表使用啟動(dòng)類(lèi)加載器作為父加載器去加載匕争。
(3)如果啟動(dòng)類(lèi)加載器加載失敗(例如在$JAVA_HOME/jre/lib里未查找到該class)爷耀,會(huì)使用拓展類(lèi)加載器來(lái)嘗試加載甘桑,繼續(xù)失敗則會(huì)使用AppClassLoader來(lái)加載,繼續(xù)失敗則會(huì)拋出一個(gè)異常ClassNotFoundException歹叮,然后再調(diào)用當(dāng)前加載器的findClass()方法進(jìn)行加載跑杭。
雙親委派模式優(yōu)勢(shì)
采用雙親委派模式的好處是:
1)Java類(lèi)隨著它的類(lèi)加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系,通過(guò)這種層級(jí)關(guān)可以避免類(lèi)的重復(fù)加載咆耿,當(dāng)父親已經(jīng)加載了該類(lèi)時(shí)德谅,就沒(méi)有必要子ClassLoader再加載一次。
2)其次是考慮到安全因素萨螺,java核心api中定義類(lèi)型不會(huì)被隨意替換窄做,假設(shè)通過(guò)網(wǎng)絡(luò)傳遞一個(gè)名為java.lang.Integer的類(lèi),通過(guò)雙親委托模式傳遞到啟動(dòng)類(lèi)加載器慰技,而啟動(dòng)類(lèi)加載器在核心Java API發(fā)現(xiàn)這個(gè)名字的類(lèi)椭盏,發(fā)現(xiàn)該類(lèi)已被加載,并不會(huì)重新加載網(wǎng)絡(luò)傳遞的過(guò)來(lái)的java.lang.Integer惹盼,而直接返回已加載過(guò)的Integer.class庸汗,這樣便可以防止核心API庫(kù)被隨意篡改。
可能你會(huì)想手报,如果我們?cè)赾lasspath路徑下自定義一個(gè)名為java.lang.SingleInterge類(lèi)(該類(lèi)是胡編的)呢蚯舱?該類(lèi)并不存在java.lang中改化,經(jīng)過(guò)雙親委托模式,傳遞到啟動(dòng)類(lèi)加載器中枉昏,由于父類(lèi)加載器路徑下并沒(méi)有該類(lèi)陈肛,所以不會(huì)加載,將反向委托給子類(lèi)加載器加載兄裂,最終會(huì)通過(guò)系統(tǒng)類(lèi)加載器加載該類(lèi)句旱。但是這樣做是不允許,因?yàn)閖ava.lang是核心API包晰奖,需要訪(fǎng)問(wèn)權(quán)限谈撒,強(qiáng)制加載將會(huì)報(bào)出如下異常:java.lang.SecurityException:Prohibited package name: java.lang。
類(lèi)加載器間的關(guān)系
我們進(jìn)一步了解類(lèi)加載器間的關(guān)系(并非指繼承關(guān)系)匾南,主要可以分為以下4點(diǎn):
啟動(dòng)類(lèi)加載器啃匿,由C++實(shí)現(xiàn),沒(méi)有父類(lèi)蛆楞。
拓展類(lèi)加載器(ExtClassLoader)溯乒,由Java語(yǔ)言實(shí)現(xiàn),父類(lèi)加載器為null
系統(tǒng)類(lèi)加載器(AppClassLoader)豹爹,由Java語(yǔ)言實(shí)現(xiàn)裆悄,父類(lèi)加載器為ExtClassLoader
自定義類(lèi)加載器,父類(lèi)加載器肯定為AppClassLoader臂聋。
類(lèi)與類(lèi)加載器
在JVM中表示兩個(gè)class對(duì)象是否為同一個(gè)類(lèi)對(duì)象存在兩個(gè)必要條件:
* 類(lèi)的完整類(lèi)名必須一致光稼,包括包名。
* 加載這個(gè)類(lèi)的ClassLoader(指ClassLoader實(shí)例對(duì)象)必須相同孩等。
顯示加載與隱式加載
所謂class文件的顯示加載與隱式加載的方式是指JVM加載class文件到內(nèi)存的方式钟哥,顯示加載指的是在代碼中通過(guò)調(diào)用ClassLoader加載class對(duì)象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加載class對(duì)象瞎访。而隱式加載則是不直接在代碼中調(diào)用ClassLoader的方法加載class對(duì)象,而是通過(guò)虛擬機(jī)自動(dòng)加載到內(nèi)存中吁恍,如在加載某個(gè)類(lèi)的class文件時(shí)扒秸,該類(lèi)的class文件中引用了另外一個(gè)類(lèi)的對(duì)象,此時(shí)額外引用的類(lèi)將通過(guò)JVM自動(dòng)加載到內(nèi)存中冀瓦。在日常開(kāi)發(fā)以上兩種方式一般會(huì)混合使用伴奥,這里我們知道有這么回事即可。
編寫(xiě)自己的類(lèi)加載器
通過(guò)前面的分析可知翼闽,實(shí)現(xiàn)自定義類(lèi)加載器需要繼承ClassLoader或者URLClassLoader拾徙,繼承ClassLoader則需要自己重寫(xiě)findClass()方法并編寫(xiě)加載邏輯,繼承URLClassLoader則可以省去編寫(xiě)findClass()方法以及class文件加載轉(zhuǎn)換成字節(jié)碼流的代碼感局。那么編寫(xiě)自定義類(lèi)加載器的意義何在呢尼啡?
* 當(dāng)class文件不在ClassPath路徑下暂衡,默認(rèn)系統(tǒng)類(lèi)加載器無(wú)法找到該class文件,在這種情況下我們需要實(shí)現(xiàn)一個(gè)自定義的ClassLoader來(lái)加載特定路徑下的class文件生成class對(duì)象崖瞭。
* 當(dāng)一個(gè)class文件是通過(guò)網(wǎng)絡(luò)傳輸并且可能會(huì)進(jìn)行相應(yīng)的加密操作時(shí)狂巢,需要先對(duì)class文件進(jìn)行相應(yīng)的解密后再加載到JVM內(nèi)存中,這種情況下也需要編寫(xiě)自定義的ClassLoader并實(shí)現(xiàn)相應(yīng)的邏輯书聚。
* 以上兩種情況在實(shí)際中的綜合運(yùn)用:比如你的應(yīng)用需要通過(guò)網(wǎng)絡(luò)來(lái)傳輸 Java 類(lèi)的字節(jié)碼唧领,為了安全性,這些字節(jié)碼經(jīng)過(guò)了加密處理雌续。這個(gè)時(shí)候你就需要自定義類(lèi)加載器來(lái)從某個(gè)網(wǎng)絡(luò)地址上讀取加密后的字節(jié)代碼斩个,接著進(jìn)行解密和驗(yàn)證,最后定義出在Java虛擬機(jī)中運(yùn)行的類(lèi)驯杜。
* 當(dāng)需要實(shí)現(xiàn)熱部署功能時(shí)(一個(gè)class文件通過(guò)不同的類(lèi)加載器產(chǎn)生不同class對(duì)象從而實(shí)現(xiàn)熱部署功能)受啥,需要實(shí)現(xiàn)自定義ClassLoader的邏輯。
自定義類(lèi)加載器
(1)從上面源碼看出艇肴,調(diào)用loadClass時(shí)會(huì)先根據(jù)委派模型在父加載器中加載腔呜,如果加載失敗,則會(huì)調(diào)用當(dāng)前加載器的findClass來(lái)完成加載再悼。
(2)因此我們自定義的類(lèi)加載器只需要繼承ClassLoader核畴,并覆蓋findClass方法,下面是一個(gè)實(shí)際例子冲九,在該例中我們用自定義的類(lèi)加載器去加載我們事先準(zhǔn)備好的class文件谤草。
自定義一個(gè)People.java類(lèi)做例子
public class People {
??? //該類(lèi)寫(xiě)在記事本里,在用javac命令行編譯成class文件莺奸,放在d盤(pán)根目錄下
??? private String name;
??? public People() {}
??? public People(Stringname) {
???????? this.name = name;
??? }
??? public String getName(){
???????? return name;
??? }
??? public voidsetName(String name) {
???????? this.name = name;
??? }
??? public String toString(){
???????? return "I am apeople, my name is " + name;
??? }
}
自定義類(lèi)加載器
自定義一個(gè)類(lèi)加載器丑孩,需要繼承ClassLoader類(lèi),并實(shí)現(xiàn)findClass方法灭贷。其中defineClass方法可以把二進(jìn)制流字節(jié)組成的文件轉(zhuǎn)換為一個(gè)java.lang.Class(只要二進(jìn)制字節(jié)流的內(nèi)容符合Class文件規(guī)范)温学。
public class MyClassLoader extends ClassLoader
{
??? public MyClassLoader()
??? {
??? }
??? public MyClassLoader(ClassLoader parent)
??? {
??????? super(parent);
??? }
??? protected ClassfindClass(String name) throws ClassNotFoundException
??? {
??????? File file = newFile("D:/People.class");
??????? try{
??????????? byte[] bytes = getClassBytes(file);
??????????? //defineClass方法可以把二進(jìn)制流字節(jié)組成的文件轉(zhuǎn)換為一個(gè)java.lang.Class
??????????? Class c = this.defineClass(name, bytes, 0, bytes.length);
??????????? return c;
??????? }
??????? catch (Exception e)
??????? {
???????????e.printStackTrace();
??????? }
??????? return super.findClass(name);
??? }
??? private byte[] getClassBytes(File file) throws Exception
??? {
??????? //這里要讀入.class的字節(jié),因此要使用字節(jié)流
??????? FileInputStream fis= new FileInputStream(file);
??????? FileChannel fc =fis.getChannel();
???????ByteArrayOutputStream baos = new ByteArrayOutputStream();
??????? WritableByteChannel wbc = Channels.newChannel(baos);
??????? ByteBuffer by = ByteBuffer.allocate(1024);
??????? while (true){
??????????? int i =fc.read(by);
??????????? if (i == 0 || i== -1)
??????????? break;
??????????? by.flip();
??????????? wbc.write(by);
??????????? by.clear();
??????? }
??????? fis.close();
??????? returnbaos.toByteArray();
??? }
??? public static voidmain(String[] args)
??? {
??????? MyClassLoader mcl =new MyClassLoader();
??????? Class?clazz;
??????? try
??????? {
??????????? clazz =Class.forName("People", true, mcl);
??????????? Object obj;
??????????? obj =clazz.newInstance();
???????????System.out.println(obj);
???????????System.out.println(obj.getClass().getClassLoader());//打印出我們的自定義類(lèi)加載器
??????? }
??????? catch(ClassNotFoundException | InstantiationException | IllegalAccessException e)
??????? {
???????????e.printStackTrace();
??????? }
??? }
}
運(yùn)行結(jié)果:
I am a
people, my name is null
com.huawei.monitor.calcsvc.utils.MyClassLoader@15db9742
Tomcat類(lèi)加載機(jī)制:
Tomcat6.0:
當(dāng)應(yīng)用需要到某個(gè)類(lèi)時(shí)甚疟,則會(huì)按照下面的順序進(jìn)行類(lèi)加載:
1)使用bootstrap引導(dǎo)類(lèi)加載器加載
2)使用system系統(tǒng)類(lèi)加載器加載
3)使用應(yīng)用類(lèi)加載器在WEB-INF/classes中加載
4)使用應(yīng)用類(lèi)加載器在WEB-INF/lib中加載
? ? ? ?5)使用common類(lèi)加載器在CATALINA_HOME/lib中加載
Tomcat7.0+:
加載類(lèi)或資源時(shí)仗岖,要查看的倉(cāng)庫(kù)及其順序如下:
1)JVM 的 Bootstrap 類(lèi)
2)應(yīng)用類(lèi)加載器加載Web 應(yīng)用的/WEB-INF/classes 類(lèi)
3)應(yīng)用類(lèi)加載器加載Web 應(yīng)用的/WEB-INF/lib/*.jar 類(lèi)
4)System 類(lèi)加載器的類(lèi)
5)Common 類(lèi)加載器的類(lèi)
如果 Web 應(yīng)用類(lèi)加載器配置有 <Loader delegate="true"/>,則順序變?yōu)椋?/p>
1)JVM 的 Bootstrap 類(lèi)
2)System 類(lèi)加載器的類(lèi)
3)Common 類(lèi)加載器的類(lèi)
4)應(yīng)用類(lèi)加載器加載Web 應(yīng)用的/WEB-INF/classes 類(lèi)
5)應(yīng)用類(lèi)加載器加載Web 應(yīng)用的/WEB-INF/lib/*.jar 類(lèi)