一往毡、類加載器的基本概念
顧名思義靶溜,類加載器(class loader)用來加載 Java 類到 Java 虛擬機中墨技。一般來說扣汪,Java 虛擬機使用 Java 類的方式如下:Java 源程序(.java 文件)在經(jīng)過 Java 編譯器編譯之后就被轉(zhuǎn)換成 Java 字節(jié)代碼(.class 文件)崭别。類加載器負(fù)責(zé)讀取 Java 字節(jié)代碼,并轉(zhuǎn)換成 java.lang.Class類的一個實例茅主。每個這樣的實例用來表示一個 Java 類诀姚。通過此實例的 newInstance()方法就可以創(chuàng)建出該類的一個對象。實際的情況可能更加復(fù)雜呀打,比如 Java 字節(jié)代碼可能是通過工具動態(tài)生成的贬丛,也可能是通過網(wǎng)絡(luò)下載的豺憔。
二够庙、類與類加載器
對于任意一個類,都需要加載它的類加載器和這個類本身來確定這個類在Java虛擬機中的唯一性暮屡,每一個類加載器都有一個獨立的類名稱空間褒纲。也就是說莺掠,如果比較兩個類是否是同一個類读宙,除了這比較這兩個類本身的全限定名是否相同之外,還要比較這兩個類是否是同一個類加載器加載的唇兑。即使同一個類文件兩次加載到同一個虛擬機中扎附,但如果是由兩個不同的類加載器加載的留夜,那這兩個類仍然不是同一個類。
這個相等性比較會影響一些方法碍粥,比如Class對象的equals()
方法嚼摩、isAssignableFrom()
方法、isInstance()
方法等蜂厅,還有instanceof
關(guān)鍵字做對象所屬關(guān)系判定等掘猿。下面的代碼演示了不同的類加載器對instanceof
關(guān)鍵字的影響:
package temp;
import java.io.IOException;
import java.io.InputStream;
public class ClassLoaderTest {
public static void main(String[] args) throws Exception{
ClassLoader loader=new ClassLoader() {
@Override
public Class<?> loadClass(String name)throws ClassNotFoundException{
try{
String filename=name.substring(name.lastIndexOf(".")+1)+".class";
InputStream is=getClass().getResourceAsStream(filename);
if(is==null){
return super.loadClass(name);
}
byte[] b=new byte[is.available()];
is.read(b);
return defineClass(name,b,0,b.length);
}catch(IOException e){
throw new ClassNotFoundException(name);
}
}
};
Object obj=loader.loadClass("temp.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj.getClass().getClassLoader());
System.out.println(ClassLoaderTest.class.getClassLoader());
System.out.println(obj instanceof temp.ClassLoaderTest);
}
}
運行結(jié)果:
這里構(gòu)造了一個簡單的類加載器稠通,它可以加載與自己在同一個路徑下的Class文件改橘。然后使用這個類加載器去加載全限定名是temp.ClassLoaderTest
的類飞主,并實例化了這個類的對象碌识。從第一行輸出可以看出虱而,這個對象確實是temp.ClassLoaderTest
類的一個實例,我們打印了一下對象obj
的類加載器和ClassLoaderTest
的類加載器魁瞪,發(fā)現(xiàn)確實是不同的兩個不同的類加載器导俘,最后輸出表明在做instanceof
檢查時出現(xiàn)了false
剔蹋,這是因為這時虛擬機中有兩個temp.ClassLoaderTest
類滩租,一個是系統(tǒng)應(yīng)用程序類加載器加載的,另一個是自定義的類加載器加載的猎莲,這兩個類雖然來自同一個Class文件著洼,但是加載它們的類加載器不同而叼,導(dǎo)致類型檢查時結(jié)果是false
。
三、雙親委派模型
Java 中的類加載器大致可以分成兩類匕垫,一類是系統(tǒng)提供的绊困,另外一類則是由 Java 應(yīng)用開發(fā)人員編寫的。系統(tǒng)提供的類加載器主要有下面三個:
- 啟動類加載器:負(fù)責(zé)將存放在
<JAVA_HOME>\lib
目錄中的煤蹭,或者被-Xbootclasspath
參數(shù)所指定的路徑中的硝皂,并且是虛擬機識別的類庫加載到虛擬機內(nèi)存中贫途。啟動類加載器無法被Java程序直接引用丢早,用戶在編寫自定義類加載器時,如果需要把加載請求委派給引導(dǎo)類加載器傀缩,那直接使用null代替即可赡艰。 - 擴展類加載器:這個加載器由
sun.misc.Launcher$ExtClassLoader
實現(xiàn)斤葱,負(fù)責(zé)加載<JAVA_HOME>\lib\ext
目錄下的揖闸,或者被java.ext.dirs
系統(tǒng)變量所指定的路徑中的所有類庫汤纸,開發(fā)者可以直接使用擴展類加載器贮泞。 - 應(yīng)用程序類加載器:這個類加載器是由
sun.misc.Launcher$AppClassLoader
實現(xiàn)的啃擦。由于這個類加載器是ClassLoader
中的getSystemClassLoader
方法的返回值饿悬,所以也叫系統(tǒng)類加載器。它負(fù)責(zé)加載用戶類路徑上所指定的類庫言询,開發(fā)者可以直接使用這個類加載器运杭,如果應(yīng)用程序中沒有自定義過自己的類加載器函卒,一般情況下這個就是程序中默認(rèn)的類加載器报嵌。
用戶的應(yīng)用程序就是在這三個類加載器的配合下加載的。不過腕巡,用戶還可以加入自己的類加載器绘沉,這些類加載器的關(guān)系如下圖:
這種類加載的層次關(guān)系车伞,稱為類加載器的雙親委派模型喻喳。雙親委派模型要求除了頂層的啟動類加載器之外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器谦去。不過這個父子關(guān)系不是通過繼承實現(xiàn)的鳄哭,而是使用組合關(guān)系來復(fù)用父加載器的代碼。
雙親委派模型的工作過程如下:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類飘痛,而是把這個請求委派給父類加載器去完成容握,每一個層次的類加載器都是如此,因此所有的加載請求最終都會傳送到頂層的啟動類加載器中塑猖,只有當(dāng)父類加載器反饋自己無法完成這個加載請求(它的搜索范圍內(nèi)沒找到這個類)時羊苟,自加載器才會嘗試自己加載感憾。
雙親委派模型是為了保證 Java 核心庫的類型安全。所有 Java 應(yīng)用都至少需要引用 java.lang.Object
類凉倚,也就是說在運行的時候稽寒,java.lang.Object
這個類需要被加載到 Java 虛擬機中趟章。如果這個加載過程由 Java 應(yīng)用自己的類加載器來完成的話尤揣,很可能就存在多個版本的 java.lang.Object
類,而且這些類之間是不兼容的负芋。通過雙親委派模型,對于 Java 核心庫的類的加載工作由啟動類加載器來統(tǒng)一完成莽龟,保證了 Java 應(yīng)用所使用的都是同一個版本的 Java 核心庫的類毯盈,是互相兼容的病袄。
不同的類加載器為相同名稱的類創(chuàng)建了額外的名稱空間。相同名稱的類可以并存在 Java 虛擬機中脑奠,只需要用不同的類加載器來加載它們即可宋欺。不同類加載器加載的類之間是不兼容的胰伍,這就相當(dāng)于在 Java 虛擬機內(nèi)部創(chuàng)建了一個個相互隔離的 Java 類空間。
四祷杈、深入探討雙親委派模型
雙親委派模型雖然保障了Java核心類庫的安全問題吠式,但是雙親委派模型也有其缺點,那就是如果基礎(chǔ)類又要調(diào)用用戶的代碼特占,那該怎么辦云茸?
比如我們經(jīng)常使用的JDBC技術(shù),學(xué)習(xí)過JDBC的應(yīng)該知道JDBC只是一組接口規(guī)范,具體的實現(xiàn)是由數(shù)據(jù)庫廠商實現(xiàn)的亡容,那么JDBC的接口代碼存在于核心類庫中,是由啟動類加載器加載的,但是JDBC的實現(xiàn)代碼是由各個廠商提供脚囊,是由系統(tǒng)類加載器加載悔耘。啟動類加載器是無法無法找到 JDBC接口 的實現(xiàn)類的,因為它只加載 Java 的核心庫缓艳。它也不能代理給系統(tǒng)類加載器郎任,因為它是系統(tǒng)類加載器的祖先類加載器备籽。也就是說车猬,類加載器的雙親委派模型無法解決這個問題珠闰。
看到這里你可能會產(chǎn)生一個疑問瘫辩,那就是為啥上層的類加載器加載的類無法訪問下層類加載器加載的類,但是下層的類加載器加載的類可以訪問上層類加載器加載的類承绸,比如:我們寫的類Person
由系統(tǒng)類加載器加載军熏,String
類由啟動類加載器加載卷扮,也就是說Person
類中可以訪問到String
,但是String
類中無法訪問到Person
(這里是一個不太恰當(dāng)?shù)睦幽︶#驗槲覀儫o法具體的測試String
類中是否可以訪問到Person
類)或衡。
我在看書的時候就產(chǎn)生了上面的疑問,經(jīng)過谷歌老師的指導(dǎo)偷办,我終于找到了問題的答案:
首先是兩個術(shù)語:在前面介紹類加載器的雙親委派模型的時候椒涯,提到過類加載器會首先代理給其它類加載器來嘗試加載某個類回梧。這就意味著真正完成類的加載工作的類加載器和啟動這個加載過程的類加載器狱意,有可能不是同一個。真正完成類的加載工作是通過調(diào)用defineClass
來實現(xiàn)的财骨;而啟動類的加載過程是通過調(diào)用loadClass
來實現(xiàn)的隆箩。前者稱為一個類的定義加載器(defining loader)羔杨,后者稱為初始加載器(initiating loader)。
注意:初始類加載器對于一個類來說經(jīng)常不是一個理澎,比如String類在加載的過程中糠爬,先是交給系統(tǒng)類加載器加載举庶,但是系統(tǒng)類加載器代理給了擴展類加載期,擴展類加載器又代理給了引導(dǎo)類加載器殴玛,最后由引導(dǎo)類加載器加載完成添祸,那么這個過程中的定義類加載器就是引導(dǎo)類加載器刃泌,但是初始類加載器是三個(系統(tǒng)類加載器署尤、擴展類加載器曹体、引導(dǎo)類加載器)硝烂,因為這三個類加載器都調(diào)用了loadClass
方法,而最后的引導(dǎo)類加載器還調(diào)用了defineClass
方法滞谢。
JVM為每個類加載器維護(hù)的一個“表”,這個表記錄了所有以此類加載器為“初始類加載器”(而不是定義類加載器狮杨,所以一個類可以存在于很多的命名空間中)加載的類的列表。屬于同一個列表的類可以互相訪問清寇。這就可以解釋為什么上層的類加載器加載的類無法訪問下層類加載器加載的類,但是下層的類加載器加載的類可以訪問上層類加載器加載的類?的疑問了搅方。
在 Java 虛擬機判斷兩個類是否相同的時候姨涡,使用的是類的定義加載器。也就是說匈仗,哪個類加載器啟動類的加載過程并不重要,重要的是最終定義這個類的加載器火架。兩種類加載器的關(guān)聯(lián)之處在于:一個類的定義加載器是它引用的其它類的初始加載器。如類 com.example.Outer
引用了類com.example.Inner
骡男,則由類 com.example.Outer
的定義加載器負(fù)責(zé)啟動類 com.example.Inner
的加載過程。
五骚亿、解決雙親委派模型的缺陷——線程上下文類加載器
線程上下文類加載器(context class loader)是從 JDK 1.2 開始引入的。類 java.lang.Thread
中的方法getContextClassLoader()
和 setContextClassLoader(ClassLoader cl)
用來獲取和設(shè)置線程的上下文類加載器。如果沒有通過setContextClassLoader(ClassLoader cl)
方法進(jìn)行設(shè)置的話泥技,線程將繼承其父線程的上下文類加載器。Java 應(yīng)用運行的初始線程的上下文類加載器是系統(tǒng)類加載器店茶。在線程中運行的代碼可以通過此類加載器來加載類和資源。
通過使用線程上下文類加載器可以實現(xiàn)父類加載器請求子類加載器去完成類加載的動作。
這里并沒有講解為啥線程上下文類加載器可以打破雙親委派模型鸯檬?為啥可以逆向使用類加載器?庐冯,如果想要了解,這里有篇文章可以參考一下:真正理解線程上下文類加載器(多案例分析)
六栖茉、另外一種加載類的方法:Class.forName
Class.forName
是一個靜態(tài)方法尘应,同樣可以用來加載類苍鲜。該方法有兩種形式:Class.forName(String name, boolean initialize, ClassLoader loader)
和 Class.forName(String className)
。第一種形式的參數(shù) name
表示的是類的全名坯屿;initialize
表示是否初始化類电湘;loader
表示加載時使用的類加載器。第二種形式則相當(dāng)于設(shè)置了參數(shù)initialize
的值為true
,loader
的值為當(dāng)前類的類加載器劫拢。Class.forName
的一個很常見的用法是在加載數(shù)據(jù)庫驅(qū)動的時候妹沙。如 Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()
用來加載 Apache Derby
數(shù)據(jù)庫的驅(qū)動牵寺。
七趣斤、開發(fā)自己的類加載器
一般來說唬渗,自己開發(fā)的類加載器只需要覆寫findClass(String name)
方法即可。java.lang.ClassLoader
類的方法loadClass()
封裝了雙親委派模型的實現(xiàn)。該方法會首先調(diào)用findLoadedClass()
方法來檢查該類是否已經(jīng)被加載過;如果沒有加載過的話浴滴,會調(diào)用父類加載器的 loadClass()
方法來嘗試加載該類;如果父類加載器無法加載該類的話屡限,就調(diào)用findClass()
方法來查找該類。因此眶诈,為了保證類加載器都正確實現(xiàn)代理模式东帅,在開發(fā)自己的類加載器時,最好不要覆寫 loadClass()
方法,而是覆寫findClass()
方法檩淋。示例如下:
public class FileClassLoader extends ClassLoader {
private String rootDir;
public FileClassLoader(String rootDir) {
this.rootDir = rootDir;
}
/**
* 編寫findClass方法的邏輯
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 獲取類的class文件字節(jié)數(shù)組
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
//直接生成class對象
return defineClass(name, classData, 0, classData.length);
}
}
/**
* 編寫獲取class文件并轉(zhuǎn)換為字節(jié)碼流的邏輯
* @param className
* @return
*/
private byte[] getClassData(String className) {
// 讀取類文件的字節(jié)
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
// 讀取類文件的字節(jié)碼
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 類文件的完全路徑
* @param className
* @return
*/
private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
public static void main(String[] args) throws ClassNotFoundException {
String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/";
//創(chuàng)建自定義文件類加載器
FileClassLoader loader = new FileClassLoader(rootDir);
try {
//加載指定的class文件
Class<?> object1=loader.loadClass("com.zejian.classloader.DemoObj");
System.out.println(object1.newInstance().toString());
//輸出結(jié)果:I am DemoObj
} catch (Exception e) {
e.printStackTrace();
}
}
}