相關(guān)好文:面試官對(duì)于JVM類加載機(jī)制的猛烈炮火,你能頂住嗎?
https://juejin.im/post/5d1d4f8ef265da1b60291fec
類是在運(yùn)行期間第一次使用時(shí)動(dòng)態(tài)加載的,而不是一次性加載。因?yàn)槿绻淮涡约虞d,那么會(huì)占用很多的內(nèi)存。
類加載的機(jī)制的層次結(jié)構(gòu)
每個(gè)編寫的”.java”拓展名類文件都存儲(chǔ)著需要執(zhí)行的程序邏輯,這些”.java”文件經(jīng)過Java編譯器編譯成拓展名為”.class”的文件,”.class”文件中保存著Java代碼經(jīng)轉(zhuǎn)換后的虛擬機(jī)指令,當(dāng)需要使用某個(gè)類時(shí),虛擬機(jī)將會(huì)加載它的”.class”文件,并創(chuàng)建對(duì)應(yīng)的class對(duì)象,將class文件加載到虛擬機(jī)的內(nèi)存,這個(gè)過程稱為類加載,這里我們需要了解一下類加載的過程,如下:

加載:類加載過程的一個(gè)階段,通過一個(gè)類的完全限定查找此類字節(jié)碼文件,并利用字節(jié)碼文件創(chuàng)建一個(gè)Class對(duì)象
1、通過一個(gè)類的全限定名獲取描述此類的二進(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è)類的java.lang.Class對(duì)象,作為訪問方法區(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)備:為類變量(即static修飾的字段變量)分配內(nèi)存并且設(shè)置該類變量的初始值,使用的是方法區(qū)的內(nèi)存。
實(shí)例變量不會(huì)在這階段分配內(nèi)存,它將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在Java堆中。
注意,實(shí)例化不是類加載的一個(gè)過程,類加載發(fā)生在所有實(shí)例化操作之前,并且類加載只進(jìn)行一次,實(shí)例化可以進(jìn)行多次。
初始值一般為 0 值,例如下面的類變量 value 被初始化為 0 而不是 123。
public static int value = 123;
如果類變量是常量,那么會(huì)按照表達(dá)式來進(jìn)行初始化,而不是賦值為 0。
public static final int value = 123;
解析:主要將常量池中的【符號(hào)引用】替換為【直接引用】的過程。符號(hào)引用就是一組符號(hào)來描述目標(biāo),可以是任何字面量,而直接引用就是直接指向目標(biāo)的指針、相對(duì)偏移量或一個(gè)間接定位到目標(biāo)的句柄。其中解析過程在某些情況下可以在初始化階段之后再開始,這是為了支持 Java 的動(dòng)態(tài)綁定。
解析的時(shí)候class已經(jīng)被加載到方法區(qū)的內(nèi)存中,因此要把符號(hào)引用轉(zhuǎn)化為直接引用,也就是能直接找到該類實(shí)際內(nèi)存地址的引用。
分為類或接口的解析,字段解析,類方法解析,接口方法解析。
初始化:類加載最后階段,若該類具有超類,則對(duì)其進(jìn)行初始化,執(zhí)行靜態(tài)初始化器和靜態(tài)初始化成員變量(如前面只初始化了默認(rèn)值的static變量將會(huì)在這個(gè)階段賦值,成員變量也將被初始化)。
初始化階段是執(zhí)行類構(gòu)造器<clinit>方法的過程,<clinit>方法由類變量的賦值動(dòng)作和靜態(tài)語句塊按照在源文件出現(xiàn)的順序合并而成,該合并操作由編譯器完成。
1)<clinit>方法對(duì)于類或接口不是必須的,如果一個(gè)類中沒有靜態(tài)代碼塊,也沒有靜態(tài)變量的賦值操作,那么編譯器不會(huì)生成<clinit>;
2)<clinit>方法與實(shí)例構(gòu)造器不同,不需要顯式的調(diào)用父類的<clinit>方法,虛擬機(jī)會(huì)保證父類的<clinit>優(yōu)先執(zhí)行;
3)為了防止多次執(zhí)行<clinit>,虛擬機(jī)會(huì)確保<clinit>方法在多線程環(huán)境下被正確的加鎖同步執(zhí)行,如果有多個(gè)線程同時(shí)初始化一個(gè)類,那么只有一個(gè)線程能夠執(zhí)行<clinit>方法,其它線程進(jìn)行阻塞等待,直到<clinit>執(zhí)行完成。
4)注意:執(zhí)行接口的<clinit>方法不需要先執(zhí)行父接口的<clinit>,只有使用父接口中定義的變量時(shí),才會(huì)執(zhí)行。
對(duì)象的初始化順序:
????????????????? 基類的static域加載
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?|
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?|
? ? ? ? ? ? ? ? ?子類的static域加載?????????????????? ?????
??????????????????????? ????? |
??????????????????????? ????? |
基類的成員變量初始化(基本類型初始化為默認(rèn)值,對(duì)象引用設(shè)為null,若有初值則初始化為初值)???????????
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?|
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?|
基類的構(gòu)造器加載(若在構(gòu)造器中調(diào)用的方法在子類中被覆蓋過,則調(diào)用覆蓋后的方法)
??????????????????????? ????? |
??????????????????????? ????? |
???????????? 子類的成員變量初始化???????????????????????????? ?????
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?|
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?|
? ? ? ? ? ? ? ? ?子類的構(gòu)造器加載
類初始化場景:
1,主動(dòng)引用
虛擬機(jī)中嚴(yán)格規(guī)定了有且只有5種情況必須對(duì)類進(jìn)行初始化。
* 執(zhí)行new、getstatic、putstatic和invokestatic指令;
* 使用reflect對(duì)類進(jìn)行反射調(diào)用;
* 初始化一個(gè)類的時(shí)候,父類還沒有初始化,會(huì)事先初始化父類;
* 啟動(dòng)虛擬機(jī)時(shí),需要初始化包含main方法的類;
* 在JDK1.7中,如果java.lang.invoke.MethodHandler實(shí)例最后的解析結(jié)果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個(gè)方法句柄對(duì)應(yīng)的類沒有進(jìn)行初始化;
2,被動(dòng)引用
以上 5 種場景中的行為稱為對(duì)一個(gè)類進(jìn)行主動(dòng)引用。除此之外,所有引用類的方式都不會(huì)觸發(fā)初始化,稱為被動(dòng)引用。被動(dòng)引用的常見例子包括:
以下幾種情況,不會(huì)觸發(fā)類初始化
1)通過子類引用父類的靜態(tài)字段,只會(huì)觸發(fā)父類的初始化,而不會(huì)觸發(fā)子類的初始化。
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ù)組,即通過數(shù)組定義來引用類,不會(huì)觸發(fā)該類的初始化。
public class Init{?
??? public static voidmain(String[] args){?
??????? Parent[] parents =new Parent[10];
??? }?
}?
無輸出,說明沒有觸發(fā)類Parent的初始化,但是這段代碼做了什么?先看看生成的字節(jié)碼指令

anewarray指令為新數(shù)組分配空間,并觸發(fā)[Lcom.ctrip.ttd.whywhy.Parent類的初始化,這個(gè)類由虛擬機(jī)自動(dòng)生成。
3)常量在編譯期間會(huì)存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用定義常量的類,不會(huì)觸發(fā)定義常量所在的類。
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
說明沒有觸發(fā)類Const的初始化,在編譯階段,Const類中常量A的值100存儲(chǔ)到Init類的常量池中,這兩個(gè)類在編譯成class文件之后就沒有聯(lián)系了。
4)通過類名獲取Class對(duì)象,不會(huì)觸發(fā)類的初始化。
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,所以通過Dog.class并不會(huì)觸發(fā)Dog類的初始化動(dòng)作。
5)通過Class.forName加載指定類時(shí),如果指定參數(shù)initialize為false時(shí),也不會(huì)觸發(fā)類初始化,其實(shí)這個(gè)參數(shù)是告訴虛擬機(jī),是否要對(duì)類進(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)通過ClassLoader默認(rèn)的loadClass方法,也不會(huì)觸發(fā)初始化動(dòng)作
new ClassLoader(){}.loadClass("zzzzzz.Cat");
JVM類加載器
這便是類加載的5個(gè)過程,而類加載器的任務(wù)是根據(jù)一個(gè)類的全限定名來讀取此類的二進(jìn)制字節(jié)流到JVM中,然后轉(zhuǎn)換為一個(gè)與目標(biāo)類對(duì)應(yīng)的java.lang.Class對(duì)象實(shí)例,在虛擬機(jī)提供了3種類加載器,啟動(dòng)(Bootstrap)類加載器、擴(kuò)展(Extension)類加載器、系統(tǒng)(System)類加載器(也稱應(yīng)用類加載器),下面分別介紹:
啟動(dòng)(Bootstrap)類加載器
啟動(dòng)類加載器主要加載的是JVM自身需要的類,這個(gè)類加載使用C++語言實(shí)現(xiàn)的,是虛擬機(jī)自身的一部分,它負(fù)責(zé)將 <JAVA_HOME>/lib路徑下的核心類庫或-Xbootclasspath參數(shù)指定的路徑下的jar包加載到內(nèi)存中,注意由于虛擬機(jī)是按照文件名識(shí)別加載jar包的,如rt.jar,如果文件名不被虛擬機(jī)識(shí)別,即使把jar包丟到lib目錄下也是沒有作用的(出于安全考慮,Bootstrap啟動(dòng)類加載器只加載包名為java、javax、sun等開頭的類)。
擴(kuò)展(Extension)類加載器
擴(kuò)展類加載器是指Sun公司實(shí)現(xiàn)的sun.misc.Launcher$ExtClassLoader類,由Java語言實(shí)現(xiàn)的,是Launcher的靜態(tài)內(nèi)部類,它負(fù)責(zé)加載<JAVA_HOME>/lib/ext目錄下或者由系統(tǒng)變量-Djava.ext.dir指定位路徑中的類庫,開發(fā)者可以直接使用標(biāo)準(zhǔn)擴(kuò)展類加載器。
應(yīng)用程序類(Application)類加載器
也稱應(yīng)用程序加載器是指Sun公司實(shí)現(xiàn)的sun.misc.Launcher$AppClassLoader。它負(fù)責(zé)加載系統(tǒng)類路徑j(luò)ava -classpath或-D java.class.path 指定路徑下的類庫,也就是我們經(jīng)常用到的classpath路徑,開發(fā)者可以直接使用系統(tǒng)類加載器,一般情況下該類加載是程序中默認(rèn)的類加載器,通過ClassLoader#getSystemClassLoader()方法可以獲取到該類加載器。
自定義(User)類加載器
JVM提供的類加載器只能加載指定目錄的類(jar和class),如果我們想從其他地方甚至網(wǎng)絡(luò)上獲取class文件,就需要自定義類加載器來實(shí)現(xiàn),自定義類加載器主要都是通過繼承ClassLoader或者它的子類來實(shí)現(xiàn),但無論是通過繼承ClassLoader還是它的子類,最終自定義類加載器的父加載器都是應(yīng)用程序類加載器,因?yàn)椴还苷{(diào)用哪個(gè)父類加載器,創(chuàng)建的對(duì)象都必須最終調(diào)用java.lang.ClassLoader.getSystemClassLoader()作為父加載器,getSystemClassLoader()方法的返回值是sun.misc.Launcher.AppClassLoader即應(yīng)用程序類加載器。
??????? 在Java的日常應(yīng)用程序開發(fā)中,類的加載幾乎是由上述3種類加載器相互配合執(zhí)行的,在必要時(shí),我們還可以自定義類加載器,需要注意的是,Java虛擬機(jī)對(duì)class文件采用的是按需加載的方式,也就是說當(dāng)需要使用該類時(shí)才會(huì)將它的class文件加載到內(nèi)存生成class對(duì)象,而且加載某個(gè)類的class文件時(shí),Java虛擬機(jī)采用的是雙親委派模式即把請(qǐng)求交由父類處理,它一種任務(wù)委派模式,下面我們進(jìn)一步了解它。
雙親委派模式工作原理
雙親委派模式要求除了頂層的啟動(dòng)類加載器外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器,請(qǐng)注意雙親委派模式中的父子關(guān)系并非通常所說的類繼承關(guān)系,而是采用組合關(guān)系來復(fù)用父類加載器的相關(guān)代碼,類加載器間的關(guān)系如下:

工作過程:
一個(gè)類加載器首先將類加載請(qǐng)求傳送到父類加載器,只有當(dāng)父類加載器無法完成類加載請(qǐng)求時(shí)才嘗試加載。

雙親委派模式是在Java 1.2后引入的,其工作原理的是,如果一個(gè)類加載器收到了類加載請(qǐng)求,它并不會(huì)自己先去加載,而是把這個(gè)請(qǐng)求委托給父類的加載器去執(zhí)行,如果父類加載器還存在其父類加載器,則進(jìn)一步向上委托,依次遞歸,請(qǐng)求最終將到達(dá)頂層的啟動(dòng)類加載器,如果父類加載器可以完成類加載任務(wù),就成功返回,倘若父類加載器無法完成此加載任務(wù),子加載器才會(huì)嘗試自己去加載,這就是雙親委派模式,即每個(gè)兒子都很懶,每次有活就丟給父親去干,直到父親說這件事我也干不了時(shí),兒子自己想辦法去完成,這不就是傳說中的實(shí)力坑爹啊?那么采用這種模式有啥用呢?
//雙親委派模型的工作過程源碼
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
??????????? //父類加載器無法完成類加載請(qǐng)求
??????? }
??????? if (c == null) {
??????????? // If still notfound, then invoke findClass in order to find the class
??????????? //子加載器進(jìn)行類加載
??????????? c =findClass(name);
??????? }
??? }
??? if (resolve) {
??????? //判斷是否需要鏈接過程,參數(shù)傳入
??????? resolveClass(c);
??? }
??? return c;
}
雙親委派模型的工作過程如下:
(1)當(dāng)前類加載器從自己已經(jīng)加載的類中查詢是否此類已經(jīng)加載,如果已經(jīng)加載則直接返回原來已經(jīng)加載的類。
(2)如果沒有找到,就去委托父類加載器去加載(如代碼c = parent.loadClass(name, false)所示)。父類加載器也會(huì)采用同樣的策略,查看自己已經(jīng)加載過的類中是否包含這個(gè)類,有就返回,沒有就委托父類的父類去加載,一直到啟動(dòng)類加載器。因?yàn)槿绻讣虞d器為空了,就代表使用啟動(dòng)類加載器作為父加載器去加載。
(3)如果啟動(dòng)類加載器加載失敗(例如在$JAVA_HOME/jre/lib里未查找到該class),會(huì)使用拓展類加載器來嘗試加載,繼續(xù)失敗則會(huì)使用AppClassLoader來加載,繼續(xù)失敗則會(huì)拋出一個(gè)異常ClassNotFoundException,然后再調(diào)用當(dāng)前加載器的findClass()方法進(jìn)行加載。
雙親委派模式優(yōu)勢
采用雙親委派模式的好處是:
1)Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系,通過這種層級(jí)關(guān)可以避免類的重復(fù)加載,當(dāng)父親已經(jīng)加載了該類時(shí),就沒有必要子ClassLoader再加載一次。
2)其次是考慮到安全因素,java核心api中定義類型不會(huì)被隨意替換,假設(shè)通過網(wǎng)絡(luò)傳遞一個(gè)名為java.lang.Integer的類,通過雙親委托模式傳遞到啟動(dòng)類加載器,而啟動(dòng)類加載器在核心Java API發(fā)現(xiàn)這個(gè)名字的類,發(fā)現(xiàn)該類已被加載,并不會(huì)重新加載網(wǎng)絡(luò)傳遞的過來的java.lang.Integer,而直接返回已加載過的Integer.class,這樣便可以防止核心API庫被隨意篡改。
可能你會(huì)想,如果我們?cè)赾lasspath路徑下自定義一個(gè)名為java.lang.SingleInterge類(該類是胡編的)呢?該類并不存在java.lang中,經(jīng)過雙親委托模式,傳遞到啟動(dòng)類加載器中,由于父類加載器路徑下并沒有該類,所以不會(huì)加載,將反向委托給子類加載器加載,最終會(huì)通過系統(tǒng)類加載器加載該類。但是這樣做是不允許,因?yàn)閖ava.lang是核心API包,需要訪問權(quán)限,強(qiáng)制加載將會(huì)報(bào)出如下異常:java.lang.SecurityException:Prohibited package name: java.lang。
類加載器間的關(guān)系
我們進(jìn)一步了解類加載器間的關(guān)系(并非指繼承關(guān)系),主要可以分為以下4點(diǎn):
啟動(dòng)類加載器,由C++實(shí)現(xiàn),沒有父類。
拓展類加載器(ExtClassLoader),由Java語言實(shí)現(xiàn),父類加載器為null
系統(tǒng)類加載器(AppClassLoader),由Java語言實(shí)現(xiàn),父類加載器為ExtClassLoader
自定義類加載器,父類加載器肯定為AppClassLoader。
類與類加載器
在JVM中表示兩個(gè)class對(duì)象是否為同一個(gè)類對(duì)象存在兩個(gè)必要條件:
* 類的完整類名必須一致,包括包名。
* 加載這個(gè)類的ClassLoader(指ClassLoader實(shí)例對(duì)象)必須相同。
顯示加載與隱式加載
所謂class文件的顯示加載與隱式加載的方式是指JVM加載class文件到內(nèi)存的方式,顯示加載指的是在代碼中通過調(diào)用ClassLoader加載class對(duì)象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加載class對(duì)象。而隱式加載則是不直接在代碼中調(diào)用ClassLoader的方法加載class對(duì)象,而是通過虛擬機(jī)自動(dòng)加載到內(nèi)存中,如在加載某個(gè)類的class文件時(shí),該類的class文件中引用了另外一個(gè)類的對(duì)象,此時(shí)額外引用的類將通過JVM自動(dòng)加載到內(nèi)存中。在日常開發(fā)以上兩種方式一般會(huì)混合使用,這里我們知道有這么回事即可。
編寫自己的類加載器
通過前面的分析可知,實(shí)現(xiàn)自定義類加載器需要繼承ClassLoader或者URLClassLoader,繼承ClassLoader則需要自己重寫findClass()方法并編寫加載邏輯,繼承URLClassLoader則可以省去編寫findClass()方法以及class文件加載轉(zhuǎn)換成字節(jié)碼流的代碼。那么編寫自定義類加載器的意義何在呢?
* 當(dāng)class文件不在ClassPath路徑下,默認(rèn)系統(tǒng)類加載器無法找到該class文件,在這種情況下我們需要實(shí)現(xiàn)一個(gè)自定義的ClassLoader來加載特定路徑下的class文件生成class對(duì)象。
* 當(dāng)一個(gè)class文件是通過網(wǎng)絡(luò)傳輸并且可能會(huì)進(jìn)行相應(yīng)的加密操作時(shí),需要先對(duì)class文件進(jìn)行相應(yīng)的解密后再加載到JVM內(nèi)存中,這種情況下也需要編寫自定義的ClassLoader并實(shí)現(xiàn)相應(yīng)的邏輯。
* 以上兩種情況在實(shí)際中的綜合運(yùn)用:比如你的應(yīng)用需要通過網(wǎng)絡(luò)來傳輸 Java 類的字節(jié)碼,為了安全性,這些字節(jié)碼經(jīng)過了加密處理。這個(gè)時(shí)候你就需要自定義類加載器來從某個(gè)網(wǎng)絡(luò)地址上讀取加密后的字節(jié)代碼,接著進(jìn)行解密和驗(yàn)證,最后定義出在Java虛擬機(jī)中運(yùn)行的類。
* 當(dāng)需要實(shí)現(xiàn)熱部署功能時(shí)(一個(gè)class文件通過不同的類加載器產(chǎn)生不同class對(duì)象從而實(shí)現(xiàn)熱部署功能),需要實(shí)現(xiàn)自定義ClassLoader的邏輯。
自定義類加載器
(1)從上面源碼看出,調(diào)用loadClass時(shí)會(huì)先根據(jù)委派模型在父加載器中加載,如果加載失敗,則會(huì)調(diào)用當(dāng)前加載器的findClass來完成加載。
(2)因此我們自定義的類加載器只需要繼承ClassLoader,并覆蓋findClass方法,下面是一個(gè)實(shí)際例子,在該例中我們用自定義的類加載器去加載我們事先準(zhǔn)備好的class文件。
自定義一個(gè)People.java類做例子
public class People {
??? //該類寫在記事本里,在用javac命令行編譯成class文件,放在d盤根目錄下
??? 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;
??? }
}
自定義類加載器
自定義一個(gè)類加載器,需要繼承ClassLoader類,并實(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());//打印出我們的自定義類加載器
??????? }
??????? 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類加載機(jī)制:
Tomcat6.0:

當(dāng)應(yīng)用需要到某個(gè)類時(shí),則會(huì)按照下面的順序進(jìn)行類加載:
1)使用bootstrap引導(dǎo)類加載器加載
2)使用system系統(tǒng)類加載器加載
3)使用應(yīng)用類加載器在WEB-INF/classes中加載
4)使用應(yīng)用類加載器在WEB-INF/lib中加載
? ? ? ?5)使用common類加載器在CATALINA_HOME/lib中加載
Tomcat7.0+:
加載類或資源時(shí),要查看的倉庫及其順序如下:
1)JVM 的 Bootstrap 類
2)應(yīng)用類加載器加載Web 應(yīng)用的/WEB-INF/classes 類
3)應(yīng)用類加載器加載Web 應(yīng)用的/WEB-INF/lib/*.jar 類
4)System 類加載器的類
5)Common 類加載器的類
如果 Web 應(yīng)用類加載器配置有 <Loader delegate="true"/>,則順序變?yōu)椋?/p>
1)JVM 的 Bootstrap 類
2)System 類加載器的類
3)Common 類加載器的類
4)應(yīng)用類加載器加載Web 應(yīng)用的/WEB-INF/classes 類
5)應(yīng)用類加載器加載Web 應(yīng)用的/WEB-INF/lib/*.jar 類