類加載的機制的層次結(jié)構(gòu)
每個編寫的”.java”拓展名類文件都存儲著需要執(zhí)行的程序邏輯吴裤,這些”.java”文件經(jīng)過Java編譯器編譯成拓展名為”.class”的文件恨溜,”.class”文件中保存著Java代碼經(jīng)轉(zhuǎn)換后的虛擬機指令,當(dāng)需要使用某個類時摆寄,虛擬機將會加載它的”.class”文件垃环,并創(chuàng)建對應(yīng)的class對象,將class文件加載到虛擬機的內(nèi)存异赫,這個過程稱為類加載椅挣,這里我們需要了解一下類加載的過程头岔,如下:
加載:類加載過程的一個階段:通過一個類的完全限定查找此類字節(jié)碼文件,并利用字節(jié)碼文件創(chuàng)建一個Class對象
驗證:目的在于確保Class文件的字節(jié)流中包含信息符合當(dāng)前虛擬機要求鼠证,不會危害虛擬機自身安全峡竣。主要包括四種驗證,文件格式驗證量九,元數(shù)據(jù)驗證适掰,字節(jié)碼驗證,符號引用驗證荠列。
準(zhǔn)備:為類變量(即static修飾的字段變量)分配內(nèi)存并且設(shè)置該類變量的初始值即0(如static int i=5;這里只將i初始化為0类浪,至于5的值將在初始化時賦值),這里不包含用final修飾的static肌似,因為final在編譯的時候就會分配了费就,注意這里不會為實例變量分配初始化,類變量會分配在方法區(qū)中锈嫩,而實例變量是會隨著對象一起分配到Java堆中受楼。
解析:主要將常量池中的符號引用替換為直接引用的過程。符號引用就是一組符號來描述目標(biāo)呼寸,可以是任何字面量艳汽,而直接引用就是直接指向目標(biāo)的指針、相對偏移量或一個間接定位到目標(biāo)的句柄对雪。有類或接口的解析河狐,字段解析,類方法解析瑟捣,接口方法解析(這里涉及到字節(jié)碼變量的引用馋艺,如需更詳細(xì)了解,可參考《深入Java虛擬機》)迈套。
初始化:類加載最后階段捐祠,若該類具有超類衷敌,則對其進(jìn)行初始化苔埋,執(zhí)行靜態(tài)初始化器和靜態(tài)初始化成員變量(如前面只初始化了默認(rèn)值的static變量將會在這個階段賦值,成員變量也將被初始化)馋辈。
這便是類加載的5個過程贵白,而類加載器的任務(wù)是根據(jù)一個類的全限定名來讀取此類的二進(jìn)制字節(jié)流到JVM中率拒,然后轉(zhuǎn)換為一個與目標(biāo)類對應(yīng)的java.lang.Class對象實例,在虛擬機提供了3種類加載器禁荒,引導(dǎo)(Bootstrap)類加載器猬膨、擴展(Extension)類加載器、系統(tǒng)(System)類加載器(也稱應(yīng)用類加載器)
ClassLoader加載機制如下:
啟動(Bootstrap)類加載器
啟動類加載器主要加載的是JVM自身需要的類呛伴,這個類加載使用C++語言實現(xiàn)的勃痴,是虛擬機自身的一部分谒所,它負(fù)責(zé)將 <JAVA_HOME>/lib路徑下的核心類庫或-Xbootclasspath參數(shù)指定的路徑下的jar包加載到內(nèi)存中,注意必由于虛擬機是按照文件名識別加載jar包的召耘,如rt.jar百炬,如果文件名不被虛擬機識別,即使把jar包丟到lib目錄下也是沒有作用的(出于安全考慮污它,Bootstrap啟動類加載器只加載包名為java剖踊、javax、sun等開頭的類)衫贬。
擴展(Extension)類加載器
擴展類加載器是指Sun公司(已被Oracle收購)實現(xiàn)的sun.misc.Launcher$ExtClassLoader類德澈,由Java語言實現(xiàn)的,是Launcher的靜態(tài)內(nèi)部類固惯,它負(fù)責(zé)加載<JAVA_HOME>/lib/ext目錄下或者由系統(tǒng)變量-Djava.ext.dir指定位路徑中的類庫梆造,開發(fā)者可以直接使用標(biāo)準(zhǔn)擴展類加載器。
//ExtClassLoader類中獲取路徑的代碼
private static File[] getExtDirs() {
//加載<JAVA_HOME>/lib/ext目錄中的類庫
String s = System.getProperty("java.ext.dirs");
File[] dirs;
if (s != null) {
StringTokenizer st =
new StringTokenizer(s, File.pathSeparator);
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs;
}
系統(tǒng)(System)類加載器
也稱應(yīng)用程序加載器是指 Sun公司實現(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()方法可以獲取到該類加載器。
在Java的日常應(yīng)用程序開發(fā)中烂斋,類的加載幾乎是由上述3種類加載器相互配合執(zhí)行的屹逛,在必要時,我們還可以自定義類加載器汛骂,需要注意的是罕模,Java虛擬機對class文件采用的是按需加載的方式,也就是說當(dāng)需要使用該類時才會將它的class文件加載到內(nèi)存生成class對象帘瞭,而且加載某個類的class文件時淑掌,Java虛擬機采用的是雙親委派模式即把請求交由父類處理,它一種任務(wù)委派模式蝶念,下面我們進(jìn)一步了解它锋拖。
理解雙親委派模式
雙親委派模式工作原理
雙親委派模式要求除了頂層的啟動類加載器外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器祸轮,請注意雙親委派模式中的父子關(guān)系并非通常所說的類繼承關(guān)系,而是采用組合關(guān)系來復(fù)用父類加載器的相關(guān)代碼侥钳,類加載器間的關(guān)系如下:
雙親委派模式是在Java 1.2后引入的适袜,其工作原理的是,如果一個類加載器收到了類加載請求舷夺,它并不會自己先去加載苦酱,而是把這個請求委托給父類的加載器去執(zhí)行售貌,如果父類加載器還存在其父類加載器,則進(jìn)一步向上委托疫萤,依次遞歸颂跨,請求最終將到達(dá)頂層的啟動類加載器,如果父類加載器可以完成類加載任務(wù)扯饶,就成功返回恒削,倘若父類加載器無法完成此加載任務(wù),子加載器才會嘗試自己去加載尾序,這就是雙親委派模式钓丰。
雙親委派模式優(yōu)勢
類加載器的雙親委托模型好處:
1、可以確保Java核心庫的類型安全:所有的Java應(yīng)用都至少會引用java.lang.Object類每币,也就是在運行期java.lang.Object類會加載到Java虛擬機中携丁,如果這個加載過程是由Java自己的類加載器所完成,那么就很可能會在JVM中存在多個版本的java.lang.Object類兰怠,而且這些類還不兼容梦鉴,并且互相不可見(正是命名空間在發(fā)揮作用),借助于雙親委托機制揭保,Java核心類庫中的類的加載工作肥橙,都是由啟動類加載器來統(tǒng)一完成加載工作,從而確保了Java應(yīng)用使用的都是同一個版本的Java核心類庫掖举,它們之間是相互兼容的快骗。
2、可以確保Java核心類庫所提供的類塔次,不會被自定義的類所替代方篮。
3、不同的類加載器可以為相同名稱(binary name)的類創(chuàng)建額外的命名空間励负,相同名稱的類可以并存在Java虛擬機中藕溅,只需要不同的類加載器來加載它們即可,不同類加載器所加載的類之間是不兼容的继榆,這就相當(dāng)于在Java虛擬機內(nèi)部創(chuàng)建了一個又一個項目隔離的Java類空間巾表,這類技術(shù)在很多框架中都得到了實際應(yīng)用。
4略吨、采用雙親委派模式的是好處是Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系集币,通過這種層級關(guān)可以避免類的重復(fù)加載,當(dāng)父親已經(jīng)加載了該類時翠忠,就沒有必要子ClassLoader再加載一次鞠苟。
Bootstrap啟動類加載器作用:
在運行期,一個Java類是由該類的完全限定名(binary name,二進(jìn)制名)和用于加載該類的定義類加載器(defining loader)所共同決定的当娱,如果同樣名字(即相同的全限定名)的類是由兩個不同的加載器所加載吃既,那么這些類就是不同的,即便.class
文件的字節(jié)碼完全一樣跨细,并且從相同的位置加載亦是如此鹦倚。
在Oracle的Hotspot實現(xiàn)中,系統(tǒng)屬性sun.boot.class.path如果修改錯了冀惭,則運行出錯震叙。
內(nèi)建于JVM中的啟動類加載器會加載java.lang.ClassLoader以及其他的Java平臺類,當(dāng)JVM啟動時云头,一塊特殊的機器碼會運行捐友,它會加載擴展類加載器與系統(tǒng)類加載器,這塊特殊的機器碼叫做啟動類加載器(Bootstrap)溃槐,啟動類加載器并不是Java類匣砖,而其他的加載器是Java類,啟動類加載器是特于平臺的機器指令昏滴,它負(fù)責(zé)開啟整個加載過程
所有類加載器(除了啟動類加載器)都被實現(xiàn)為Java類猴鲫,不過總歸要有一個組件來加載第一個Java類加載器,從而讓整個加載過程能夠順利進(jìn)行下去谣殊,加載第一個純Java類加載器就是啟動類加載器的職責(zé)
啟動類加載器還會負(fù)責(zé)加載供JRE正常運行所需要的基本組件拂共,即rt包下的類,包括java.util和java.lang包等
下面代碼可以看到各類加載器的加載路徑:
public class MyTest22 {
public static void main(String[] args) {
System.out.println(System.getProperty("sun.boot.class.path"));
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println(System.getProperty("java.class.path"));
System.out.println(ClassLoader.class.getClassLoader());
System.out.println(Launcher.class.getClassLoader());
}
}
下面我們從代碼層面了解幾個Java中定義的類加載器及其雙親委派模式的實現(xiàn)姻几,它們類圖關(guān)系如下
從圖可以看出頂層的類加載器是ClassLoader類宜狐,它是一個抽象類,其后所有的類加載器都繼承自ClassLoader(不包括啟動類加載器)蛇捌,這里我們主要介紹ClassLoader中幾個比較重要的方法抚恒。
-
loadClass(String name, boolean resolve)
該方法加載指定名稱(包括包名)的二進(jìn)制類型,該方法在JDK1.2之后不再建議用戶重寫但用戶可以直接調(diào)用該方法络拌,loadClass()方法是ClassLoader類自己實現(xiàn)的俭驮,該方法中的邏輯就是雙親委派模式的實現(xiàn),其源碼如下春贸,loadClass(String name, boolean resolve)是一個重載方法混萝,resolve參數(shù)代表是否生成class對象的同時進(jìn)行解析相關(guān)操作。:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// // 先從緩存查找該class對象萍恕,找到就不用重新加載
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果找不到逸嘀,則委托給父類加載器去加載
c = parent.loadClass(name, false);
} else {
//如果沒有父類,則委托給啟動加載器去加載
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 如果都沒有找到允粤,則通過自定義實現(xiàn)的findClass去查找并加載
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//是否需要在加載時進(jìn)行解析
resolveClass(c);
}
return c;
}
}
正如loadClass方法所展示的厘熟,當(dāng)類加載請求到來時屯蹦,先從緩存中查找該類對象,如果存在直接返回绳姨,如果不存在則交給該類加載器的父加載器去加載,倘若沒有父加載器則交給頂級啟動類加載器去加載阔挠,最后倘若仍沒有找到飘庄,則使用findClass()方法去加載(關(guān)于findClass()稍后會進(jìn)一步介紹)。從loadClass實現(xiàn)也可以知道如果不想重新定義加載類的規(guī)則购撼,也沒有復(fù)雜的邏輯跪削,只想在運行時加載自己指定的類,那么我們可以直接使用this.getClass().getClassLoder.loadClass("className")迂求,這樣就可以直接調(diào)用ClassLoader的loadClass方法獲取到class對象碾盐。
-
findClass(String name)
在JDK1.2之前,在自定義類加載時揩局,總會去繼承ClassLoader類并重寫loadClass方法毫玖,從而實現(xiàn)自定義的類加載類,但是在JDK1.2之后已不再建議用戶去覆蓋loadClass()方法凌盯,而是建議把自定義的類加載邏輯寫在findClass()方法中付枫,從前面的分析可知,findClass()方法是在loadClass()方法中被調(diào)用的驰怎,當(dāng)loadClass()方法中父加載器加載失敗后阐滩,則會調(diào)用自己的findClass()方法來完成類加載,這樣就可以保證自定義的類加載器也符合雙親委托模式县忌。需要注意的是ClassLoader類中并沒有實現(xiàn)findClass()方法的具體代碼邏輯掂榔,取而代之的是拋出ClassNotFoundException異常,同時應(yīng)該知道的是findClass方法通常是和defineClass方法一起使用的(稍后會分析)症杏,ClassLoader類中findClass()方法源碼如下:
protected Class<?> findClass(String name) throws ClassNotFoundException {
//直接拋出異常
throw new ClassNotFoundException(name);
}
-
defineClass(byte[] b, int off, int len)
defineClass()方法是用來將byte字節(jié)流解析成JVM能夠識別的Class對象(ClassLoader中已實現(xiàn)該方法邏輯)装获,通過這個方法不僅能夠通過class文件實例化class對象,也可以通過其他方式實例化class對象鸳慈,如通過網(wǎng)絡(luò)接收一個類的字節(jié)碼饱溢,然后轉(zhuǎn)換為byte字節(jié)流創(chuàng)建對應(yīng)的Class對象,defineClass()方法通常與findClass()方法一起使用走芋,一般情況下绩郎,在自定義類加載器時,會直接覆蓋ClassLoader的findClass()方法并編寫加載規(guī)則翁逞,取得要加載類的字節(jié)碼后轉(zhuǎn)換成流肋杖,然后調(diào)用defineClass()方法生成類的Class對象,簡單例子如下:
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
// 獲取類的字節(jié)數(shù)組
byte[] data = this.loadClassData(className);
if(data == null){
throw new ClassNotFoundException();
}
//使用defineClass生成class對象
return this.defineClass(className,data,0,data.length);
}
}
需要注意的是挖函,如果直接調(diào)用defineClass()方法生成類的Class對象状植,這個類的Class對象并沒有解析(也可以理解為鏈接階段,畢竟解析是鏈接的最后一步),其解析操作需要等待初始化階段進(jìn)行津畸。
-
resolveClass(Class<?> c)
使用該方法可以使用類的Class對象創(chuàng)建完成也同時被解析振定。前面我們說鏈接階段主要是對字節(jié)碼進(jìn)行驗證,為類變量分配內(nèi)存并設(shè)置初始值同時將字節(jié)碼文件中的符號引用轉(zhuǎn)換為直接引用肉拓。
上述4個方法是ClassLoader類中的比較重要的方法后频,也是我們可能會經(jīng)常用到的方法。接看SercureClassLoader擴展了 ClassLoader暖途,新增了幾個與使用相關(guān)的代碼源(對代碼源的位置及其證書的驗證)和權(quán)限定義類驗證(主要指對class源碼的訪問權(quán)限)的方法卑惜,一般我們不會直接跟這個類打交道,更多是與它的子類URLClassLoader有所關(guān)聯(lián)驻售,前面說過露久,ClassLoader是一個抽象類,很多方法是空的沒有實現(xiàn)欺栗,比如 findClass()毫痕、findResource()等。而URLClassLoader這個實現(xiàn)類為這些方法提供了具體的實現(xiàn)纸巷,并新增了URLClassPath類協(xié)助取得Class字節(jié)碼流等功能镇草,在編寫自定義類加載器時,如果沒有太過于復(fù)雜的需求瘤旨,可以直接繼承URLClassLoader類梯啤,這樣就可以避免自己去編寫findClass()方法及其獲取字節(jié)碼流的方式,使自定義類加載器編寫更加簡潔存哲,下面是URLClassLoader的類圖(利用IDEA生成的類圖)
從類圖結(jié)構(gòu)看出URLClassLoader中存在一個URLClassPath類因宇,通過這個類就可以找到要加載的字節(jié)碼流,也就是說URLClassPath類負(fù)責(zé)找到要加載的字節(jié)碼祟偷,再讀取成字節(jié)流察滑,最后通過defineClass()方法創(chuàng)建類的Class對象。
從URLClassLoader類的結(jié)構(gòu)圖可以看出其構(gòu)造方法都有一個必須傳遞的參數(shù)URL[]修肠,該參數(shù)的元素是代表字節(jié)碼文件的路徑,換句話說在創(chuàng)建URLClassLoader對象時必須要指定這個類加載器的到那個目錄下找class文件贺辰。
同時也應(yīng)該注意URL[]也是URLClassPath類的必傳參數(shù),在創(chuàng)建URLClassPath對象時嵌施,會根據(jù)傳遞過來的URL數(shù)組中的路徑判斷是文件還是jar包饲化,然后根據(jù)不同的路徑創(chuàng)建FileLoader或者JarLoader或默認(rèn)Loader類去加載相應(yīng)路徑下的class文件,而當(dāng)JVM調(diào)用findClass()方法時吗伤,就由這3個加載器中的一個將class文件的字節(jié)碼流加載到內(nèi)存中吃靠,最后利用字節(jié)碼流創(chuàng)建類的class對象。
請記住足淆,如果我們在定義類加載器時選擇繼承ClassLoader類而非URLClassLoader巢块,必須手動編寫findclass()方法的加載邏輯以及獲取字節(jié)碼流的邏輯礁阁。
了解完URLClassLoader后接著看看剩余的兩個類加載器,即拓展類加載器ExtClassLoader和系統(tǒng)類加載器AppClassLoader族奢,這兩個類都繼承自URLClassLoader姥闭,是sun.misc.Launcher
的靜態(tài)內(nèi)部類。sun.misc.Launcher
主要被系統(tǒng)用于啟動主應(yīng)用程序歹鱼,ExtClassLoader和AppClassLoader都是由sun.misc.Launcher
創(chuàng)建的泣栈,其類主要類結(jié)構(gòu)如下:
它們間的關(guān)系正如前面所闡述的那樣,同時我們發(fā)現(xiàn)ExtClassLoader并沒有重寫loadClass()方法弥姻,這足矣說明其遵循雙親委派模式,而AppClassLoader重載了loadCass()方法掺涛,但最終調(diào)用的還是父類loadClass()方法庭敦,因此依然遵守雙親委派模式,重載方法源碼如下:
/**
* Override loadClass 方法薪缆,新增包權(quán)限檢測功能
*/
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
int var3 = var1.lastIndexOf(46);
if (var3 != -1) {
SecurityManager var4 = System.getSecurityManager();
if (var4 != null) {
var4.checkPackageAccess(var1.substring(0, var3));
}
}
if (this.ucp.knownToNotExist(var1)) {
Class var5 = this.findLoadedClass(var1);
if (var5 != null) {
if (var2) {
this.resolveClass(var5);
}
return var5;
} else {
throw new ClassNotFoundException(var1);
}
} else {
//依然調(diào)用父類的方法
return super.loadClass(var1, var2);
}
}
其實無論是ExtClassLoader還是AppClassLoader都繼承URLClassLoader類秧廉,因此它們都遵守雙親委托模型,這點是毋庸置疑的拣帽。
到此我們對ClassLoader疼电、URLClassLoader、ExtClassLoader减拭、AppClassLoader以及Launcher類間的關(guān)系有了比較清晰的了解蔽豺,同時對一些主要的方法也有一定的認(rèn)識,這里并沒有對這些類的源碼進(jìn)行詳細(xì)的分析拧粪,畢竟沒有那個必要修陡,因為我們主要弄得類與類間的關(guān)系和常用的方法同時搞清楚雙親委托模式的實現(xiàn)過程,為編寫自定義類加載器做鋪墊就足夠了可霎。前面出現(xiàn)了很多父類加載器的說法魄鸦,但每個類加載器的父類到底是誰,一直沒有闡明癣朗,下面我們就通過代碼驗證的方式來闡明這答案拾因。
類加載器間的關(guān)系
我們進(jìn)一步了解類加載器間的關(guān)系(并非指繼承關(guān)系),主要可以分為以下4點:
- 啟動類加載器旷余,由C++實現(xiàn)绢记,沒有父類。
- 拓展類加載器(ExtClassLoader)荣暮,由Java語言實現(xiàn)庭惜,父類加載器為null
- 系統(tǒng)類加載器(AppClassLoader),由Java語言實現(xiàn)穗酥,父類加載器為ExtClassLoader
- 自定義類加載器护赊,父類加載器肯定為AppClassLoader惠遏。
下面我們通過程序來驗證上述闡述的觀點
public class MyClassLoader extends ClassLoader {
private String classLoaderName;
private String path;
private final String fileExtension = ".class";
public MyClassLoader(String classLoaderName){
super();//將系統(tǒng)類加載器作為該類加載器的父加載器
this.classLoaderName = classLoaderName;
}
public MyClassLoader(ClassLoader parent,String classLoaderName){
super(parent);//顯示指定該類加載器的父加載器
this.classLoaderName = classLoaderName;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
@Override
public String toString() {
return "MyTest15{" +
"classLoaderName='" + classLoaderName + '\'' +
'}';
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
System.out.println("findClass invoked:"+className);
System.out.println("class loader name:"+this.classLoaderName);
byte[] data = this.loadClassData(className);
if(data == null){
throw new ClassNotFoundException();
}
return this.defineClass(className,data,0,data.length);
}
private byte[] loadClassData(String className){
InputStream inputStream = null;
byte[] data = null;
ByteArrayOutputStream bos = null;
try {
this.path = path.replace(".","/");
this.classLoaderName = classLoaderName.replace(".","/");
inputStream = new FileInputStream(new File(this.path + className + this.classLoaderName));
bos = new ByteArrayOutputStream();
int ch;
while((ch = inputStream.read()) != -1){
bos.write(ch);
}
data = bos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}finally {
if(inputStream != null){
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(bos != null){
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return data;
}
public static void main(String[] args) throws Exception {
MyClassLoader loader1 = new MyClassLoader("loader1");
System.out.println("自定義類加載器的父加載器: "+loader1.getParent());
System.out.println("系統(tǒng)默認(rèn)的AppClassLoader: "+ClassLoader.getSystemClassLoader());
System.out.println("AppClassLoader的父類加載器: "+ClassLoader.getSystemClassLoader().getParent());
System.out.println("ExtClassLoader的父類加載器: "+ClassLoader.getSystemClassLoader().getParent().getParent());
System.out.println("------------------");
MyClassLoader myClassLoader1 = new MyClassLoader("loader1");
myClassLoader1.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
Class<?> clazz1 = myClassLoader1.loadClass("com.yibo.jvm.classloader.MyTest1");
System.out.println("clazz1:" + clazz1);
Object object1 = clazz1.newInstance();
System.out.println(object1);
System.out.println(myClassLoader1.getClass().getClassLoader());
System.out.println("------------------");
MyClassLoader myClassLoader2 = new MyClassLoader(myClassLoader1,"loader12");
myClassLoader2.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
Class<?> clazz2 = myClassLoader2.loadClass("com.yibo.jvm.classloader.MyTest1");
System.out.println("clazz2:" + clazz2);
Object object2 = clazz2.newInstance();
System.out.println(object2);
System.out.println("------------------");
MyClassLoader myClassLoader3 = new MyClassLoader("loader12");
myClassLoader3.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
Class<?> clazz3 = myClassLoader3.loadClass("com.yibo.jvm.classloader.MyTest1");
System.out.println("clazz3:" + clazz3);
Object object3 = clazz3.newInstance();
System.out.println(object3);
}
}
代碼中,我們自定義了一個MyClassLoader骏啰,這里我們繼承了ClassLoader而非URLClassLoader,因此需要自己編寫findClass()方法邏輯以及加載字節(jié)碼的邏輯节吮,接著在main方法中,通過ClassLoader.getSystemClassLoader()獲取到系統(tǒng)默認(rèn)類加載器判耕,通過獲取其父類加載器及其父父類加載器透绩,同時還獲取了自定義類加載器的父類加載器,最終輸出結(jié)果正如我們所預(yù)料的,AppClassLoader的父類加載器為ExtClassLoader壁熄,而ExtClassLoader沒有父類加載器帚豪。如果我們實現(xiàn)自己的類加載器,它的父加載器都只會是AppClassLoader草丧。這里我們不妨看看Lancher的構(gòu)造器源碼:
public Launcher() {
Launcher.ExtClassLoader var1;
try {
// 首先創(chuàng)建擴展類加載器
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//再創(chuàng)建AppClassLoader并把擴展類加載器作為父加載器傳遞給AppClassLoader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
//設(shè)置線程上下文類加載器狸臣,稍后分析
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
顯然Lancher初始化時首先會創(chuàng)建ExtClassLoader類加載器,然后再創(chuàng)建AppClassLoader并把ExtClassLoader傳遞給它作為父類加載器昌执,這里還把AppClassLoader默認(rèn)設(shè)置為線程上下文類加載器烛亦,關(guān)于線程上下文類加載器稍后會分析。那ExtClassLoader類加載器為什么是null呢懂拾?看下面的源碼創(chuàng)建過程就明白煤禽,在創(chuàng)建ExtClassLoader強制設(shè)置了其父加載器為null。
// 首先創(chuàng)建擴展類加載器
var1 = Launcher.ExtClassLoader.getExtClassLoader();
static class ExtClassLoader extends URLClassLoader {
private static volatile Launcher.ExtClassLoader instance;
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
if (instance == null) {
Class var0 = Launcher.ExtClassLoader.class;
synchronized(Launcher.ExtClassLoader.class) {
if (instance == null) {
instance = createExtClassLoader();
}
}
}
return instance;
}
public ExtClassLoader(File[] var1) throws IOException {
//調(diào)用父類構(gòu)造URLClassLoader傳遞null作為parent
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
}
public URLClassLoader(URL[] urls, ClassLoader parent,
URLStreamHandlerFactory factory) {
super(parent);
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
acc = AccessController.getContext();
ucp = new URLClassPath(urls, factory, acc);
}
顯然ExtClassLoader的父類為null岖赋,而AppClassLoader的父加載器為ExtClassLoader檬果,所有自定義的類加載器其父加載器只會是AppClassLoader,注意這里所指的父類并不是Java繼承關(guān)系中的那種父子關(guān)系贾节。
類與類加載器
在JVM中表示兩個class對象是否為同一個類對象存在兩個必要條件
- 類的全限定名必須一致汁汗,包括包名。
- 加載這個類的ClassLoader(指ClassLoader實例對象)必須相同栗涂。
也就是說知牌,在JVM中,即使這個兩個類對象(class對象)來源同一個Class文件斤程,被同一個虛擬機所加載角寸,但只要加載它們的ClassLoader實例對象不同,那么這兩個類對象也是不相等的忿墅,這是因為不同的ClassLoader實例對象都擁有不同的獨立的類名稱空間扁藕,所以加載的class對象也會存在不同的類名空間中,但前提是覆寫loadclass方法疚脐,從前面雙親委派模式對loadClass()方法的源碼分析中可以知亿柑,在方法第一步會通過Class<?> c = findLoadedClass(name);從緩存查找,類名完整名稱相同則不會再次被加載棍弄,因此我們必須繞過緩存查詢才能重新加載class對象望薄。當(dāng)然也可直接調(diào)用findClass()方法疟游,這樣也避免從緩存查找。如果不從緩存查詢相同完全類名的class對象痕支,那么只有ClassLoader的實例對象不同颁虐,同一字節(jié)碼文件創(chuàng)建的class對象自然也不會相同。
了解class文件的顯示加載與隱式加載的概念
所謂class文件的顯示加載與隱式加載的方式是指JVM加載class文件到內(nèi)存的方式卧须,顯示加載指的是在代碼中通過調(diào)用ClassLoader加載class對象另绩,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加載class對象。
隱式加載則是不直接在代碼中調(diào)用ClassLoader的方法加載class對象花嘶,而是通過虛擬機自動加載到內(nèi)存中笋籽,如在加載某個類的class文件時,該類的class文件中引用了另外一個類的對象椭员,此時額外引用的類將通過JVM自動加載到內(nèi)存中干签。在日常開發(fā)以上兩種方式一般會混合使用,這里我們知道有這么回事即可拆撼。
編寫自己的類加載器
通過前面的分析可知,實現(xiàn)自定義類加載器需要繼承ClassLoader或者URLClassLoader喘沿,繼承ClassLoader則需要自己重寫findClass()方法并編寫加載邏輯闸度,繼承URLClassLoader則可以省去編寫findClass()方法以及class文件加載轉(zhuǎn)換成字節(jié)碼流的代碼。那么編寫自定義類加載器的意義何在呢蚜印?
- 當(dāng)class文件不在ClassPath路徑下莺禁,默認(rèn)系統(tǒng)類加載器無法找到該class文件,在這種情況下我們需要實現(xiàn)一個自定義的ClassLoader來加載特定路徑下的class文件生成class對象窄赋。
- 當(dāng)一個class文件是通過網(wǎng)絡(luò)傳輸并且可能會進(jìn)行相應(yīng)的加密操作時哟冬,需要先對class文件進(jìn)行相應(yīng)的解密后再加載到JVM內(nèi)存中,這種情況下也需要編寫自定義的ClassLoader并實現(xiàn)相應(yīng)的邏輯忆绰。
- 當(dāng)需要實現(xiàn)熱部署功能時(一個class文件通過不同的類加載器產(chǎn)生不同class對象從而實現(xiàn)熱部署功能)浩峡,需要實現(xiàn)自定義ClassLoader的邏輯。
繼承ClassLoader實現(xiàn)自定義類加載器
public class MyClassLoader extends ClassLoader {
private String classLoaderName;
private String path;
private final String fileExtension = ".class";
public MyClassLoader(String classLoaderName){
super();//將系統(tǒng)類加載器作為該類加載器的父加載器
this.classLoaderName = classLoaderName;
}
public MyClassLoader(ClassLoader parent,String classLoaderName){
super(parent);//顯示指定該類加載器的父加載器
this.classLoaderName = classLoaderName;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
@Override
public String toString() {
return "MyTest15{" +
"classLoaderName='" + classLoaderName + '\'' +
'}';
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
System.out.println("findClass invoked:"+className);
System.out.println("class loader name:"+this.classLoaderName);
byte[] data = this.loadClassData(className);
if(data == null){
throw new ClassNotFoundException();
}
return this.defineClass(className,data,0,data.length);
}
private byte[] loadClassData(String className){
InputStream inputStream = null;
byte[] data = null;
ByteArrayOutputStream bos = null;
try {
this.path = path.replace(".","/");
this.classLoaderName = classLoaderName.replace(".","/");
inputStream = new FileInputStream(new File(this.path + className + this.classLoaderName));
bos = new ByteArrayOutputStream();
int ch;
while((ch = inputStream.read()) != -1){
bos.write(ch);
}
data = bos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}finally {
if(inputStream != null){
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(bos != null){
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return data;
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader1 = new MyClassLoader("loader1");
myClassLoader1.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
Class<?> clazz1 = myClassLoader1.loadClass("com.yibo.jvm.classloader.MyTest1");
System.out.println("clazz1:" + clazz1);
Object object1 = clazz1.newInstance();
System.out.println(object1);
System.out.println(myClassLoader1.getClass().getClassLoader());
System.out.println("------------------");
MyClassLoader myClassLoader2 = new MyClassLoader(myClassLoader1,"loader12");
myClassLoader2.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
Class<?> clazz2 = myClassLoader2.loadClass("com.yibo.jvm.classloader.MyTest1");
System.out.println("clazz2:" + clazz2);
Object object2 = clazz2.newInstance();
System.out.println(object2);
System.out.println("------------------");
MyClassLoader myClassLoader3 = new MyClassLoader("loader12");
myClassLoader3.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
Class<?> clazz3 = myClassLoader3.loadClass("com.yibo.jvm.classloader.MyTest1");
System.out.println("clazz3:" + clazz3);
Object object3 = clazz3.newInstance();
System.out.println(object3);
}
}
顯然我們通過loadClassData()方法找到class文件并轉(zhuǎn)換為字節(jié)流错敢,并重寫findClass()方法翰灾,利用defineClass()方法創(chuàng)建了類的class對象。在main方法中調(diào)用了setPath()方法加載指定路徑下的class文件稚茅,由于啟動類加載器纸淮、拓展類加載器以及系統(tǒng)類加載器都無法在其路徑下找到該類,因此最終將有自定義類加載器加載亚享,即調(diào)用findClass()方法進(jìn)行加載咽块。如果繼承URLClassLoader實現(xiàn),那代碼就更簡潔了欺税,如下:
繼承URLClassLoader現(xiàn)自定義類加載器
public class MyURLClassLoader extends URLClassLoader {
public MyURLClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
public MyURLClassLoader(URL[] urls) {
super(urls);
}
public MyURLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
super(urls, parent, factory);
}
public static void main(String[] args) throws MalformedURLException {
String rootDir="E:/gradleproject/jvm_study/build/classes/java/main/";
//創(chuàng)建自定義文件類加載器
File file = new File(rootDir);
//File to URI
URI uri=file.toURI();
URL[] urls = {uri.toURL()};
MyURLClassLoader loader = new MyURLClassLoader(urls);
try {
//加載指定的class文件
Class<?> object =loader.loadClass("com.yibo.jvm.classloader.MyTest1");
System.out.println(object.newInstance().toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
非常簡潔除了需要重寫構(gòu)造器外無需編寫findClass()方法及其class文件的字節(jié)流轉(zhuǎn)換邏輯侈沪。
自定義網(wǎng)絡(luò)類加載器
自定義網(wǎng)絡(luò)類加載器揭璃,主要用于讀取通過網(wǎng)絡(luò)傳遞的class文件(在這里我們省略class文件的解密過程),并將其轉(zhuǎn)換成字節(jié)流生成對應(yīng)的class對象峭竣,如下
public class NetClassLoader extends ClassLoader {
private String url;//class文件的URL
public NetClassLoader(String url) {
this.url = url;
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
byte[] data = this.loadClassData(className);
if(data == null){
throw new ClassNotFoundException();
}
return this.defineClass(className,data,0,data.length);
}
private byte[] loadClassData(String className){
String path = classNameToPath(className);
InputStream ins = null;
byte[] data = null;
ByteArrayOutputStream baos = null;
try {
URL url = new URL(path);
ins = url.openStream();
baos = new ByteArrayOutputStream();
int bufferSize = 8192;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
// 讀取類文件的字節(jié)
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
//這里省略解密的過程.......
data = baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}finally {
if(ins != null){
try {
ins.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(baos != null){
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return data;
}
private String classNameToPath(String className) {
// 得到類文件的URL
return url + "/" + className.replace('.', '/') + ".class";
}
}
主要是在獲取字節(jié)碼流時的區(qū)別塘辅,從網(wǎng)絡(luò)直接獲取到字節(jié)流再轉(zhuǎn)車字節(jié)數(shù)組然后利用defineClass方法創(chuàng)建class對象,如果繼承URLClassLoader類則和前面文件路徑的實現(xiàn)是類似的皆撩,無需擔(dān)心路徑是filePath還是Url扣墩,因為URLClassLoader內(nèi)的URLClassPath對象會根據(jù)傳遞過來的URL數(shù)組中的路徑判斷是文件還是jar包,然后根據(jù)不同的路徑創(chuàng)建FileLoader或者JarLoader或默認(rèn)類Loader去讀取對于的路徑或者url下的class文件扛吞。
熱部署類加載器
所謂的熱部署就是利用同一個class文件不同的類加載器在內(nèi)存創(chuàng)建出兩個不同的class對象(關(guān)于這點的原因前面已分析過呻惕,即利用不同的類加載實例),由于JVM在加載類之前會檢測請求的類是否已加載過(即在loadClass()方法中調(diào)用findLoadedClass()方法)滥比,如果被加載過亚脆,則直接從緩存獲取,不會重新加載盲泛。注意同一個類加載器的實例和同一個class文件只能被加載器一次濒持,多次加載將報錯,因此我們實現(xiàn)的熱部署必須讓同一個class文件可以根據(jù)不同的類加載器重復(fù)加載寺滚,以實現(xiàn)所謂的熱部署柑营。實際上前面的實現(xiàn)的MyClassLoader和MyURLClassLoader已具備這個功能,但前提是直接調(diào)用findClass()方法村视,而不是調(diào)用loadClass()方法官套,因為ClassLoader中l(wèi)oadClass()方法體中調(diào)用findLoadedClass()方法進(jìn)行了檢測是否已被加載,因此我們直接調(diào)用findClass()方法就可以繞過這個問題蚁孔,當(dāng)然也可以重新loadClass方法奶赔,但強烈不建議這么干。
雙親委派模型的破壞者-線程上下文類加載器
當(dāng)前類加載器(current ClassLoader)
- 每個類都會使用自己的類加載器(即加載自身的類加載器)來加載其他類(值的是所依賴的類)
- 如果ClassX引用了ClassY杠氢,那么ClassX的類加載器就會去加載ClassY(前提是ClassY尚未被加載)
線程上下文類加載器(Context ClassLoader)
- 線程上下文類加載器(Context ClassLoader)是從JDK1.2開始引入的站刑,類Thread中的getContextClassLoader()與setContextClassLoader(ClassLoader cl)分別用來獲取和設(shè)置線程上下文類加載器
- 如果沒有通過setContextClassLoader(ClassLoader cl)進(jìn)行設(shè)置的話,線程將繼承其父線程的上下文類加載器
- Java應(yīng)用運行時的初始線程的上下文類加載器是系統(tǒng)類加載器修然,在線程中運行的代碼可以通過該類加載器來加載類與資源
線程上下文類加載器的重要性:
- 在Java應(yīng)用中存在著很多服務(wù)提供者接口(Service Provider Interface笛钝,SPI),這些接口允許第三方為它們提供實現(xiàn)愕宋,如常見的 SPI 有 JDBC玻靡、JNDI等,這些 SPI 的接口屬于 Java 核心庫中贝,一般存在rt.jar包中囤捻,由Bootstrap類加載器加載,而 SPI 的第三方實現(xiàn)代碼則是作為Java應(yīng)用所依賴的 jar 包被存放在classpath路徑下邻寿,由于SPI接口中的代碼經(jīng)常需要加載具體的第三方實現(xiàn)類并調(diào)用其相關(guān)方法蝎土,但SPI的核心接口類是由引導(dǎo)類加載器來加載的视哑,而Bootstrap類加載器無法直接加載SPI的實現(xiàn)類
- 父ClassLoader可以使用當(dāng)前線程Thread.currentThread().getContextClassLoader所指定的ClassLoader加載的類,這就改變了父ClassLoader不能使用子ClassLoader或是其他沒有直接父子關(guān)系的ClassLoader加載的類的情況誊涯,即改變了雙親委托模型
- 線程上下文類加載器就是當(dāng)前線程的Current ClassLoader挡毅,在雙親委托模型下,類加載是由下至上的暴构,即下層的類加載器會委托上層進(jìn)行加載跪呈。但是對于SPI來說,有些接口是Java核心庫所提供的取逾,而Java核心庫是由啟動類加載器所加載的耗绿,而這些接口的實現(xiàn)確來自于不同的jar包(廠商提供),Java的啟動類加載器是不會加載其他來源的jar包砾隅,這樣傳統(tǒng)的雙親委托模型就無法滿足SPI的要求误阻,而通過給當(dāng)前線程設(shè)置上下文類加載器,就可以由設(shè)置的上下文類加載器來實現(xiàn)對于接口的實現(xiàn)類的加載晴埂。
如下圖所示究反,以jdbc.jar加載為例:
從圖可知rt.jar核心包是由Bootstrap類加載器加載的,其內(nèi)包含SPI核心接口類儒洛,由于SPI中的類經(jīng)常需要調(diào)用外部實現(xiàn)類的方法奴紧,而jdbc.jar包含外部實現(xiàn)類(jdbc.jar存在于classpath路徑)無法通過Bootstrap類加載器加載,因此只能委派線程上下文類加載器把jdbc.jar中的實現(xiàn)類加載到內(nèi)存以便SPI相關(guān)類使用晶丘。顯然這種線程上下文類加載器的加載方式破壞了“雙親委派模型”,它在執(zhí)行過程中拋棄雙親委派加載鏈模式唐含,使程序可以逆向使用類加載器浅浮,當(dāng)然這也使得Java類加載器變得更加靈活。為了進(jìn)一步證實這種場景捷枯,不妨看看DriverManager類的源碼滚秩,DriverManager是Java核心rt.jar包中的類,該類用來管理不同數(shù)據(jù)庫的實現(xiàn)驅(qū)動即Driver淮捆,它們都實現(xiàn)了Java核心包中的java.sql.Driver接口郁油,如mysql驅(qū)動包中的com.mysql.jdbc.Driver,這里主要看看如何加載外部實現(xiàn)類攀痊,在DriverManager初始化時會執(zhí)行如下代碼
//DriverManager是Java核心包rt.jar的類
public class DriverManager {
//省略不必要的代碼......
static {
loadInitialDrivers();//執(zhí)行該方法
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
//省略不必要的代碼......
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//加載外部的Driver的實現(xiàn)類
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
//省略不必要的代碼......
}
}
在DriverManager類初始化時執(zhí)行了loadInitialDrivers()方法,在該方法中通過ServiceLoader.load(Driver.class);去加載外部實現(xiàn)的驅(qū)動類桐腌,ServiceLoader類會去讀取mysql的jdbc.jar下META-INF文件的內(nèi)容,如下所示:
而com.mysql.jdbc.Driver繼承類如下:
/**
* Backwards compatibility to support apps that call <code>Class.forName("com.mysql.jdbc.Driver");</code>.
*/
public class Driver extends com.mysql.cj.jdbc.Driver {
public Driver() throws SQLException {
super();
}
static {
System.err.println("Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. "
+ "The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.");
}
}
從注釋可以看出平常我們使用com.mysql.jdbc.Driver已被丟棄了苟径,取而代之的是com.mysql.cj.jdbc.Driver
案站,也就是說官方不再建議我們使用如下代碼注冊mysql驅(qū)動
//不建議使用該方式注冊驅(qū)動類
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/cm-storylocker?characterEncoding=UTF-8";
// 通過java庫獲取數(shù)據(jù)庫連接
Connection conn = java.sql.DriverManager.getConnection(url, "root", "root@555");
而是直接去掉注冊步驟,如下即可:
String url = "jdbc:mysql://localhost:3306/cm-storylocker?characterEncoding=UTF-8";
// 通過java庫獲取數(shù)據(jù)庫連接
Connection conn = java.sql.DriverManager.getConnection(url, "root", "root@555");
這樣ServiceLoader會幫助我們處理一切棘街,并最終通過load()方法加載蟆盐,看看load()方法實現(xiàn)
public static <S> ServiceLoader<S> load(Class<S> service) {
//通過線程上下文類加載器加載
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
很明顯了確實通過線程上下文類加載器加載的承边,實際上核心包的SPI類對外部實現(xiàn)類的加載都是基于線程上下文類加載器執(zhí)行的,通過這種方式實現(xiàn)了Java核心代碼內(nèi)部去調(diào)用外部實現(xiàn)類石挂。我們知道線程上下文類加載器默認(rèn)情況下就是AppClassLoader博助,那為什么不直接通過getSystemClassLoader()獲取類加載器來加載classpath路徑下的類的呢?其實是可行的痹愚,但這種直接使用getSystemClassLoader()方法獲取AppClassLoader加載類有一個缺點富岳,那就是代碼部署到不同服務(wù)時會出現(xiàn)問題,如把代碼部署到Java Web應(yīng)用服務(wù)或者EJB之類的服務(wù)將會出問題里伯,因為這些服務(wù)使用的線程上下文類加載器并非AppClassLoader城瞎,而是Java Web應(yīng)用服自家的類加載器,類加載器不同疾瓮。脖镀,所以我們應(yīng)用該少用getSystemClassLoader()±堑纾總之不同的服務(wù)使用的可能默認(rèn)ClassLoader是不同的脂矫,但使用線程上下文類加載器總能獲取到與當(dāng)前程序執(zhí)行相同的ClassLoader,從而避免不必要的問題缀匕。ok~.關(guān)于線程上下文類加載器暫且聊到這泪姨,前面闡述的DriverManager類,大家可以自行看看源碼削祈,相信會有更多的體會翅溺,另外關(guān)于ServiceLoader本篇并沒有過多的闡述,畢竟我們主題是類加載器髓抑,但ServiceLoader是個很不錯的解耦機制咙崎,大家可以自行查閱其相關(guān)用法。
線程上下文類加載器的一般使用模式(獲取 - 使用 - 還原)
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try{
Thread.currentThread().setContextClassLoader(targetTClassLoader);
myMethod();
}finally{
Thread.currentThread().setContextClassLoader(classLoader);
}
- myMethod()里面調(diào)用了Thread.currentThread().getContextClassLoader()獲取當(dāng)前線程的上下文類加載器做某些事情
- 如果一個類由類加載器A加載吨拍,那么這個類的依賴類也是由相同的類加載器所加載的(前提是該類沒有被加載)
- ContextClassLoader的作用就是為了破壞Java類加載的雙親委托機制
- 當(dāng)高層提供了統(tǒng)一的接口讓低層去實現(xiàn)褪猛,同時又要在高層加載(或?qū)嵗?低層的類時,就必須要通過線程上下文類加載器來幫助高層的ClassLoader找到并加載該類
線程上下文類加載器的適用場景:
當(dāng)高層提供了統(tǒng)一接口讓低層去實現(xiàn)羹饰,同時又要是在高層加載(或?qū)嵗┑蛯拥念悤r伊滋,必須通過線程上下文類加載器來幫助高層的ClassLoader找到并加載該類。
當(dāng)使用本類托管類加載队秩,然而加載本類的ClassLoader未知時笑旺,為了隔離不同的調(diào)用者,可以取調(diào)用者各自的線程上下文類加載器代為托管馍资。