Java類(lèi)加載原理解析
摘要:
每個(gè)java開(kāi)發(fā)人員對java.lang.ClassNotFoundExcetpion這個(gè)異??隙ǘ疾荒吧?,這背后就涉及到了java技術(shù)體系中的類(lèi)加載。Java的類(lèi)加載機制是java技術(shù)體系中比較核心的部分,雖然和大部分開(kāi)發(fā)人員直接打交道不多,但是對其背后的機理有一定理解有助于排查程序中出現的類(lèi)加載失敗等技術(shù)問(wèn)題,對理解java虛擬機的連接模型和java語(yǔ)言的動(dòng)態(tài)性都有很大幫助。
由于關(guān)于java類(lèi)加載的內容較多,所以打算分三篇文章簡(jiǎn)述一下:
第一篇:java類(lèi)加載原理解析
第二篇:插件環(huán)境下類(lèi)加載原理解析
第三篇:線(xiàn)程上下文類(lèi)加載器
分類(lèi):開(kāi)發(fā)技術(shù)->J2EE
標簽:Java類(lèi)加載 類(lèi)加載器 雙親委派機制 自定義類(lèi)加載器
作者:朱興 創(chuàng )建于2007-6-22 MSN:zhu_xing@live.cn
我們首先看一下JVM預定義的三種類(lèi)型類(lèi)加載器,當一個(gè) JVM 啟動(dòng)的時(shí)候,Java 缺省開(kāi)始使用如下三種類(lèi)型類(lèi)裝入器:
啟動(dòng)(Bootstrap)類(lèi)加載器:引導類(lèi)裝入器是用本地代碼實(shí)現的類(lèi)裝入器,它負責將 <Java_Runtime_Home>/lib 下面的類(lèi)庫加載到內存中。由于引導類(lèi)加載器涉及到虛擬機本地實(shí)現細節,開(kāi)發(fā)者無(wú)法直接獲取到啟動(dòng)類(lèi)加載器的引用,所以不允許直接通過(guò)引用進(jìn)行操作。
標準擴展(Extension)類(lèi)加載器:擴展類(lèi)加載器是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader) 實(shí)現的。它負責將 < Java_Runtime_Home >/lib/ext 或者由系統變量 java.ext.dir 指定位置中的類(lèi)庫加載到內存中。開(kāi)發(fā)者可以直接使用標準擴展類(lèi)加載器。
系統(System)類(lèi)加載器:系統類(lèi)加載器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實(shí)現的。它負責將系統類(lèi)路徑(CLASSPATH)中指定的類(lèi)庫加載到內存中。開(kāi)發(fā)者可以直接使用系統類(lèi)加載器。
除了以上列舉的三種類(lèi)加載器,還有一種比較特殊的類(lèi)型就是線(xiàn)程上下文類(lèi)加載器,這個(gè)將在后面單獨介紹。
在這里,需要著(zhù)重說(shuō)明的是,JVM在加載類(lèi)時(shí)默認采用的是雙親委派機制。通俗的講,就是某個(gè)特定的類(lèi)加載器在接到加載類(lèi)的請求時(shí),首先將加載任務(wù)委托給父類(lèi)加載器,依次遞歸,如果父類(lèi)加載器可以完成類(lèi)加載任務(wù),就成功返回;只有父類(lèi)加載器無(wú)法完成此加載任務(wù)時(shí),才自己去加載。關(guān)于虛擬機默認的雙親委派機制,我們可以從系統類(lèi)加載器和標準擴展類(lèi)加載器為例作簡(jiǎn)單分析。
圖一 標準擴展類(lèi)加載器繼承層次圖
通過(guò)圖一和圖二我們可以看出,類(lèi)加載器均是繼承自java.lang.ClassLoader抽象類(lèi)。我們下面我們就看簡(jiǎn)要介紹一下java.lang.ClassLoader中幾個(gè)最重要的方法:
//加載指定名稱(chēng)(包括包名)的二進(jìn)制類(lèi)型,供用戶(hù)調用的接口
public Class<?> loadClass(String name) throws ClassNotFoundException{//…}
//加載指定名稱(chēng)(包括包名)的二進(jìn)制類(lèi)型,同時(shí)指定是否解析(但是,這里的resolve參數不一定真正能達到解析的效果~_~),供繼承用
protectedsynchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{//…}
//findClass方法一般被loadClass方法調用去加載指定名稱(chēng)類(lèi),供繼承用
protected Class<?> findClass(String name) throws ClassNotFoundException {//…}
//定義類(lèi)型,一般在findClass方法中讀取到對應字節碼后調用,可以看出不可繼承(說(shuō)明:JVM已經(jīng)實(shí)現了對應的具體功能,解析對應的字節碼,產(chǎn)生對應的內部數據結構放置到方法區,所以無(wú)需覆寫(xiě),直接調用就可以了)
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError{//…}
通過(guò)進(jìn)一步分析標準擴展類(lèi)加載器(sun.misc.Launcher$ExtClassLoader)和系統類(lèi)加載器(sun.misc.Launcher$AppClassLoader)的代碼以及其公共父類(lèi)(java.net.URLClassLoader和java.security.SecureClassLoader)的代碼可以看出,都沒(méi)有覆寫(xiě)java.lang.ClassLoader中默認的加載委派規則---loadClass(…)方法。既然這樣,我們就可以通過(guò)分析java.lang.ClassLoader中的loadClass(String name)方法的代碼就可以分析出虛擬機默認采用的雙親委派機制到底是什么模樣:
public Class<?> loadClass(String name)throws ClassNotFoundException {
return loadClass(name, false);
}
protectedsynchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 首先判斷該類(lèi)型是否已經(jīng)被加載
Class c = findLoadedClass(name);
if (c == null) {
//如果沒(méi)有被加載,就委托給父類(lèi)加載或者委派給啟動(dòng)類(lèi)加載器加載
try {
if (parent != null) {
//如果存在父類(lèi)加載器,就委派給父類(lèi)加載器加載
c = parent.loadClass(name, false);
} else {
//如果不存在父類(lèi)加載器,就檢查是否是由啟動(dòng)類(lèi)加載器加載的類(lèi),通過(guò)調用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父類(lèi)加載器和啟動(dòng)類(lèi)加載器都不能完成加載任務(wù),才調用自身的加載功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
通過(guò)上面的代碼分析,我們可以對JVM采用的雙親委派類(lèi)加載機制有了更感性的認識,下面我們就接著(zhù)分析一下啟動(dòng)類(lèi)加載器、標準擴展類(lèi)加載器和系統類(lèi)加載器三者之間的關(guān)系??赡艽蠹乙呀?jīng)從各種資料上面看到了如下類(lèi)似的一幅圖片:
上面圖片給人的直觀(guān)印象是系統類(lèi)加載器的父類(lèi)加載器是標準擴展類(lèi)加載器,標準擴展類(lèi)加載器的父類(lèi)加載器是啟動(dòng)類(lèi)加載器,下面我們就用代碼具體測試一下:
示例代碼:
try {
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent();
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
} catch (Exception e) {
e.printStackTrace();
}
}
說(shuō)明:通過(guò)java.lang.ClassLoader.getSystemClassLoader()可以直接獲取到系統類(lèi)加載器。
代碼輸出如下:
sun.misc.Launcher$AppClassLoader@197d257
sun.misc.Launcher$ExtClassLoader@7259da
null
通過(guò)以上的代碼輸出,我們可以判定系統類(lèi)加載器的父加載器是標準擴展類(lèi)加載器,但是我們試圖獲取標準擴展類(lèi)加載器的父類(lèi)加載器時(shí)確得到了null,就是說(shuō)標準擴展類(lèi)加載器本身強制設定父類(lèi)加載器為null。我們還是借助于代碼分析一下:
我們首先看一下java.lang.ClassLoader抽象類(lèi)中默認實(shí)現的兩個(gè)構造函數:
protected ClassLoader() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
//默認將父類(lèi)加載器設置為系統類(lèi)加載器,getSystemClassLoader()獲取系統類(lèi)加載器
this.parent = getSystemClassLoader();
initialized = true;
}
protected ClassLoader(ClassLoader parent) {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
//強制設置父類(lèi)加載器
this.parent = parent;
initialized = true;
}
我們再看一下ClassLoader抽象類(lèi)中parent成員的聲明:
// The parent class loader for delegation
private ClassLoader parent;
聲明為私有變量的同時(shí)并沒(méi)有對外提供可供派生類(lèi)訪(fǎng)問(wèn)的public或者protected設置器接口(對應的setter方法),結合前面的測試代碼的輸出,我們可以推斷出:
1. 系統類(lèi)加載器(AppClassLoader)調用ClassLoader(ClassLoader parent)構造函數將父類(lèi)加載器設置為標準擴展類(lèi)加載器(ExtClassLoader)。(因為如果不強制設置,默認會(huì )通過(guò)調用getSystemClassLoader()方法獲取并設置成系統類(lèi)加載器,這顯然和測試輸出結果不符。)
2. 擴展類(lèi)加載器(ExtClassLoader)調用ClassLoader(ClassLoader parent)構造函數將父類(lèi)加載器設置為null。(因為如果不強制設置,默認會(huì )通過(guò)調用getSystemClassLoader()方法獲取并設置成系統類(lèi)加載器,這顯然和測試輸出結果不符。)
現在我們可能會(huì )有這樣的疑問(wèn):擴展類(lèi)加載器(ExtClassLoader)的父類(lèi)加載器被強制設置為null了,那么擴展類(lèi)加載器為什么還能將加載任務(wù)委派給啟動(dòng)類(lèi)加載器呢?
圖四 標準擴展類(lèi)加載器和系統類(lèi)加載器成員大綱視圖
圖五 擴展類(lèi)加載器和系統類(lèi)加載器公共父類(lèi)成員大綱視圖
通過(guò)圖四和圖五可以看出,標準擴展類(lèi)加載器和系統類(lèi)加載器及其父類(lèi)(java.net.URLClassLoader和java.security.SecureClassLoader)都沒(méi)有覆寫(xiě)java.lang.ClassLoader中默認的加載委派規則---loadClass(…)方法。有關(guān)java.lang.ClassLoader中默認的加載委派規則前面已經(jīng)分析過(guò),如果父加載器為null,則會(huì )調用本地方法進(jìn)行啟動(dòng)類(lèi)加載嘗試。所以,圖三中,啟動(dòng)類(lèi)加載器、標準擴展類(lèi)加載器和系統類(lèi)加載器之間的委派關(guān)系事實(shí)上是仍就成立的。(在后面的用戶(hù)自定義類(lèi)加載器部分,還會(huì )做更深入的分析)。
以上已經(jīng)簡(jiǎn)要介紹了虛擬機默認使用的啟動(dòng)類(lèi)加載器、標準擴展類(lèi)加載器和系統類(lèi)加載器,并以三者為例結合JDK代碼對JVM默認使用的雙親委派類(lèi)加載機制做了分析。下面我們就來(lái)看一個(gè)綜合的例子。首先在eclipse中建立一個(gè)簡(jiǎn)單的java應用工程,然后寫(xiě)一個(gè)簡(jiǎn)單的JavaBean如下:
package classloader.test.bean;
publicclass TestBean {
public TestBean() {}
}
在現有當前工程中另外建立一測試類(lèi)(ClassLoaderTest.java)內容如下:
測試一:
publicclass ClassLoaderTest {
publicstaticvoid main(String[] args) {
try {
//查看當前系統類(lèi)路徑中包含的路徑條目
System.out.println(System.getProperty("java.class.path"));
//調用加載當前類(lèi)的類(lèi)加載器(這里即為系統類(lèi)加載器)加載TestBean
Class typeLoaded = Class.forName("classloader.test.bean.TestBean");
//查看被加載的TestBean類(lèi)型是被那個(gè)類(lèi)加載器加載的
System.out.println(typeLoaded.getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
對應的輸出如下:
D:"DEMO"dev"Study"ClassLoaderTest"bin
sun.misc.Launcher$AppClassLoader@197d257
(說(shuō)明:當前類(lèi)路徑默認的含有的一個(gè)條目就是工程的輸出目錄)
測試二:
將當前工程輸出目錄下的…/classloader/test/bean/TestBean.class打包進(jìn)test.jar剪貼到< Java_Runtime_Home >/lib/ext目錄下(現在工程輸出目錄下和JRE擴展目錄下都有待加載類(lèi)型的class文件)。再運行測試一測試代碼,結果如下:
D:"DEMO"dev"Study"ClassLoaderTest"bin
sun.misc.Launcher$ExtClassLoader@7259da
對比測試一和測試二,我們明顯可以驗證前面說(shuō)的雙親委派機制,系統類(lèi)加載器在接到加載classloader.test.bean.TestBean類(lèi)型的請求時(shí),首先將請求委派給父類(lèi)加載器(標準擴展類(lèi)加載器),標準擴展類(lèi)加載器搶先完成了加載請求。
測試三:
將test.jar拷貝一份到< Java_Runtime_Home >/lib下,運行測試代碼,輸出如下:
D:"DEMO"dev"Study"ClassLoaderTest"bin
sun.misc.Launcher$ExtClassLoader@7259da
測試三和測試二輸出結果一致。那就是說(shuō),放置到< Java_Runtime_Home >/lib目錄下的TestBean對應的class字節碼并沒(méi)有被加載,這其實(shí)和前面講的雙親委派機制并不矛盾。虛擬機出于安全等因素考慮,不會(huì )加載< Java_Runtime_Home >/lib存在的陌生類(lèi),開(kāi)發(fā)者通過(guò)將要加載的非JDK自身的類(lèi)放置到此目錄下期待啟動(dòng)類(lèi)加載器加載是不可能的。做個(gè)進(jìn)一步驗證,刪除< Java_Runtime_Home >/lib/ext目錄下和工程輸出目錄下的TestBean對應的class文件,然后再運行測試代碼,則將會(huì )有ClassNotFoundException異常拋出。有關(guān)這個(gè)問(wèn)題,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中設置相應斷點(diǎn)運行測試三進(jìn)行調試,會(huì )發(fā)現findBootstrapClass0()會(huì )拋出異常,然后在下面的findClass方法中被加載,當前運行的類(lèi)加載器正是擴展類(lèi)加載器(sun.misc.Launcher$ExtClassLoader),這一點(diǎn)可以通過(guò)JDT中變量視圖查看驗證。
Java的連接模型允許用戶(hù)運行時(shí)擴展引用程序,既可以通過(guò)當前虛擬機中預定義的加載器加載編譯時(shí)已知的類(lèi)或者接口,又允許用戶(hù)自行定義類(lèi)裝載器,在運行時(shí)動(dòng)態(tài)擴展用戶(hù)的程序。通過(guò)用戶(hù)自定義的類(lèi)裝載器,你的程序可以裝載在編譯時(shí)并不知道或者尚未存在的類(lèi)或者接口,并動(dòng)態(tài)連接它們并進(jìn)行有選擇的解析。
運行時(shí)動(dòng)態(tài)擴展java應用程序有如下兩個(gè)途徑:
這個(gè)方法其實(shí)在前面已經(jīng)討論過(guò),在后面的問(wèn)題2解答中說(shuō)明了該方法調用會(huì )觸發(fā)那個(gè)類(lèi)加載器開(kāi)始加載任務(wù)。這里需要說(shuō)明的是多參數版本的forName(…)方法:
public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException
這里的initialize參數是很重要的,可以覺(jué)得被加載同時(shí)是否完成初始化的工作(說(shuō)明: 單參數版本的forName方法默認是不完成初始化的).有些場(chǎng)景下,需要將initialize設置為true來(lái)強制加載同時(shí)完成初始化,例如典型的就是利用DriverManager進(jìn)行JDBC驅動(dòng)程序類(lèi)注冊的問(wèn)題,因為每一個(gè)JDBC驅動(dòng)程序類(lèi)的靜態(tài)初始化方法都用DriverManager注冊驅動(dòng)程序,這樣才能被應用程序使用,這就要求驅動(dòng)程序類(lèi)必須被初始化,而不單單被加載.
通過(guò)前面的分析,我們可以看出,除了和本地實(shí)現密切相關(guān)的啟動(dòng)類(lèi)加載器之外,包括標準擴展類(lèi)加載器和系統類(lèi)加載器在內的所有其他類(lèi)加載器我們都可以當做自定義類(lèi)加載器來(lái)對待,唯一區別是是否被虛擬機默認使用。前面的內容中已經(jīng)對java.lang.ClassLoader抽象類(lèi)中的幾個(gè)重要的方法做了介紹,這里就簡(jiǎn)要敘述一下一般用戶(hù)自定義類(lèi)加載器的工作流程吧(可以結合后面問(wèn)題解答一起看):
1、首先檢查請求的類(lèi)型是否已經(jīng)被這個(gè)類(lèi)裝載器裝載到命名空間中了,如果已經(jīng)裝載,直接返回;否則轉入步驟2
2、委派類(lèi)加載請求給父類(lèi)加載器(更準確的說(shuō)應該是雙親類(lèi)加載器,真個(gè)虛擬機中各種類(lèi)加載器最終會(huì )呈現樹(shù)狀結構),如果父類(lèi)加載器能夠完成,則返回父類(lèi)加載器加載的Class實(shí)例;否則轉入步驟3
3、調用本類(lèi)加載器的findClass(…)方法,試圖獲取對應的字節碼,如果獲取的到,則調用defineClass(…)導入類(lèi)型到方法區;如果獲取不到對應的字節碼或者其他原因失敗,返回異常給loadClass(…), loadClass(…)轉拋異常,終止加載過(guò)程(注意:這里的異常種類(lèi)不止一種)。
(說(shuō)明:這里說(shuō)的自定義類(lèi)加載器是指JDK 1.2以后版本的寫(xiě)法,即不覆寫(xiě)改變java.lang.loadClass(…)已有委派邏輯情況下)
在Java中,一個(gè)類(lèi)用其完全匹配類(lèi)名(fully qualified class name)作為標識,這里指的完全匹配類(lèi)名包括包名和類(lèi)名。但在JVM中一個(gè)類(lèi)用其全名和一個(gè)加載類(lèi)ClassLoader的實(shí)例作為唯一標識,不同類(lèi)加載器加載的類(lèi)將被置于不同的命名空間.我們可以用兩個(gè)自定義類(lèi)加載器去加載某自定義類(lèi)型(注意,不要將自定義類(lèi)型的字節碼放置到系統路徑或者擴展路徑中,否則會(huì )被系統類(lèi)加載器或擴展類(lèi)加載器搶先加載),然后用獲取到的兩個(gè)Class實(shí)例進(jìn)行java.lang.Object.equals(…)判斷,將會(huì )得到不相等的結果。這個(gè)大家可以寫(xiě)兩個(gè)自定義的類(lèi)加載器去加載相同的自定義類(lèi)型,然后做個(gè)判斷;同時(shí),可以測試加載java.*類(lèi)型,然后再對比測試一下測試結果。
Class.forName(String name)默認會(huì )使用調用類(lèi)的類(lèi)加載器來(lái)進(jìn)行類(lèi)加載。我們直接來(lái)分析一下對應的jdk的代碼:
//java.lang.Class.java
publicstatic Class<?> forName(String className)throws ClassNotFoundException {
return forName0(className, true, ClassLoader.getCallerClassLoader());
}
//java.lang.ClassLoader.java
// Returns the invoker's class loader, or null if none.
static ClassLoader getCallerClassLoader() {
// 獲取調用類(lèi)(caller)的類(lèi)型
Class caller = Reflection.getCallerClass(3);
// This can be null if the VM is requesting it
if (caller == null) {
returnnull;
}
// 調用java.lang.Class中本地方法獲取加載該調用類(lèi)(caller)的ClassLoader
return caller.getClassLoader0();
}
//java.lang.Class.java
//虛擬機本地實(shí)現,獲取當前類(lèi)的類(lèi)加載器,前面介紹的Class的getClassLoader()也使用此方法
native ClassLoader getClassLoader0();
前面講過(guò),在不指定父類(lèi)加載器的情況下,默認采用系統類(lèi)加載器??赡苡腥擞X(jué)得不明白,現在我們來(lái)看一下JDK對應的代碼實(shí)現。眾所周知,我們編寫(xiě)自定義的類(lèi)加載器直接或者間接繼承自java.lang.ClassLoader抽象類(lèi),對應的無(wú)參默認構造函數實(shí)現如下:
//摘自java.lang.ClassLoader.java
protected ClassLoader() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
this.parent = getSystemClassLoader();
initialized = true;
}
我們再來(lái)看一下對應的getSystemClassLoader()方法的實(shí)現:
privatestaticsynchronizedvoid initSystemClassLoader() {
//...
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
scl = l.getClassLoader();
//...
}
我們可以寫(xiě)簡(jiǎn)單的測試代碼來(lái)測試一下:
System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());
本機對應輸出如下:
sun.misc.Launcher$AppClassLoader@197d257
所以,我們現在可以相信當自定義類(lèi)加載器沒(méi)有指定父類(lèi)加載器的情況下,默認的父類(lèi)加載器即為系統類(lèi)加載器。同時(shí),我們可以得出如下結論:
即時(shí)用戶(hù)自定義類(lèi)加載器不指定父類(lèi)加載器,那么,同樣可以加載如下三個(gè)地方的類(lèi):
1. <Java_Runtime_Home>/lib下的類(lèi)
2. < Java_Runtime_Home >/lib/ext下或者由系統變量java.ext.dir指定位置中的類(lèi)
3. 當前工程類(lèi)路徑下或者由系統變量java.class.path指定位置中的類(lèi)
JVM規范中規定如果用戶(hù)自定義的類(lèi)加載器將父類(lèi)加載器強制設置為null,那么會(huì )自動(dòng)將啟動(dòng)類(lèi)加載器設置為當前用戶(hù)自定義類(lèi)加載器的父類(lèi)加載器(這個(gè)問(wèn)題前面已經(jīng)分析過(guò)了)。同時(shí),我們可以得出如下結論:
即時(shí)用戶(hù)自定義類(lèi)加載器不指定父類(lèi)加載器,那么,同樣可以加載到<Java_Runtime_Home>/lib下的類(lèi),但此時(shí)就不能夠加載<Java_Runtime_Home>/lib/ext目錄下的類(lèi)了。
說(shuō)明:?jiǎn)?wèn)題3和問(wèn)題4的推斷結論是基于用戶(hù)自定義的類(lèi)加載器本身延續了java.lang.ClassLoader.loadClass(…)默認委派邏輯,如果用戶(hù)對這一默認委派邏輯進(jìn)行了改變,以上推斷結論就不一定成立了,詳見(jiàn)問(wèn)題5。
1. 一般盡量不要覆寫(xiě)已有的loadClass(…)方法中的委派邏輯
一般在JDK 1.2之前的版本才這樣做,而且事實(shí)證明,這樣做極有可能引起系統默認的類(lèi)加載器不能正常工作。在JVM規范和JDK文檔中(1.2或者以后版本中),都沒(méi)有建議用戶(hù)覆寫(xiě)loadClass(…)方法,相比而言,明確提示開(kāi)發(fā)者在開(kāi)發(fā)自定義的類(lèi)加載器時(shí)覆寫(xiě)findClass(…)邏輯。舉一個(gè)例子來(lái)驗證該問(wèn)題:
//用戶(hù)自定義類(lèi)加載器WrongClassLoader.Java(覆寫(xiě)loadClass邏輯)
publicclassWrongClassLoaderextends ClassLoader {
public Class<?> loadClass(String name) throws ClassNotFoundException {
returnthis.findClass(name);
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
//假設此處只是到工程以外的特定目錄D:/library下去加載類(lèi)
具體實(shí)現代碼省略
}
}
通過(guò)前面的分析我們已經(jīng)知道,用戶(hù)自定義類(lèi)加載器(WrongClassLoader)的默
認的類(lèi)加載器是系統類(lèi)加載器,但是現在問(wèn)題4種的結論就不成立了。大家可以簡(jiǎn)
單測試一下,現在<Java_Runtime_Home>/lib、< Java_Runtime_Home >/lib/ext和工
程類(lèi)路徑上的類(lèi)都加載不上了。
//問(wèn)題5測試代碼一
publicclass WrongClassLoaderTest {
publicstaticvoid main(String[] args) {
try {
WrongClassLoader loader = new WrongClassLoader();
Class classLoaded = loader.loadClass("beans.Account");
System.out.println(classLoaded.getName());
System.out.println(classLoaded.getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
(說(shuō)明:D:"classes"beans"Account.class物理存在的)
輸出結果:
java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系統找不到指定的路徑。)
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.<init>(FileInputStream.java:106)
at WrongClassLoader.findClass(WrongClassLoader.java:40)
at WrongClassLoader.loadClass(WrongClassLoader.java:29)
at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:620)
at java.lang.ClassLoader.defineClass(ClassLoader.java:400)
at WrongClassLoader.findClass(WrongClassLoader.java:43)
at WrongClassLoader.loadClass(WrongClassLoader.java:29)
at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:620)
at java.lang.ClassLoader.defineClass(ClassLoader.java:400)
at WrongClassLoader.findClass(WrongClassLoader.java:43)
at WrongClassLoader.loadClass(WrongClassLoader.java:29)
at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)
這說(shuō)明,連要加載的類(lèi)型的超類(lèi)型java.lang.Object都加載不到了。這里列舉的由于覆寫(xiě)loadClass(…)引起的邏輯錯誤明顯是比較簡(jiǎn)單的,實(shí)際引起的邏輯錯誤可能復雜的多。
//問(wèn)題5測試二
//用戶(hù)自定義類(lèi)加載器WrongClassLoader.Java(不覆寫(xiě)loadClass邏輯)
publicclassWrongClassLoaderextends ClassLoader {
protected Class<?> findClass(String name) throws ClassNotFoundException {
//假設此處只是到工程以外的特定目錄D:/library下去加載類(lèi)
具體實(shí)現代碼省略
}
}
將自定義類(lèi)加載器代碼WrongClassLoader.Java做以上修改后,再運行測試代碼,輸出結果如下:
beans.Account
WrongClassLoader@1c78e57
這說(shuō)明,beans.Account加載成功,且是由自定義類(lèi)加載器WrongClassLoader加載。
這其中的原因分析,我想這里就不必解釋了,大家應該可以分析的出來(lái)了。
2. 2、正確設置父類(lèi)加載器
通過(guò)上面問(wèn)題4和問(wèn)題5的分析我們應該已經(jīng)理解,個(gè)人覺(jué)得這是自定義用戶(hù)類(lèi)加載器時(shí)最重要的一點(diǎn),但常常被忽略或者輕易帶過(guò)。有了前面JDK代碼的分析作為基礎,我想現在大家都可以隨便舉出例子了。
3. 3、保證findClass(String )方法的邏輯正確性
事先盡量準確理解待定義的類(lèi)加載器要完成的加載任務(wù),確保最大程度上能夠獲取到對應的字節碼內容。
一是可以直接調用ClassLoader.getSystemClassLoader()或者其他方式獲取到系統類(lèi)加載器(系統類(lèi)加載器和擴展類(lèi)加載器本身都派生自URLClassLoader),調用URLClassLoader中的getURLs()方法可以獲取到;
二是可以直接通過(guò)獲取系統屬性java.class.path 來(lái)查看當前類(lèi)路徑上的條目信息 , System.getProperty("java.class.path")
方法之一:
try {
URL[] extURLs = ((URLClassLoader)ClassLoader.getSystemClassLoader().getParent()).getURLs();
for (int i = 0; i < extURLs.length; i++) {
System.out.println(extURLs[i]);
}
} catch (Exception e) {//…}
本機對應輸出如下:
file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/dnsns.jar
file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/localedata.jar
file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/sunjce_provider.jar
file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/sunpkcs11.jar
寫(xiě)這篇文章的初衷是通過(guò)分析JDK相關(guān)代碼來(lái)驗證一些加載規則,核心就是借助雙親委派機制來(lái)分析一個(gè)加載請求處理的主要過(guò)程,所列舉的幾個(gè)簡(jiǎn)單的例子實(shí)際意義不大,因為遇到的情況往往比例子情況復雜的多。
下一篇文章,我會(huì )重點(diǎn)分析一下Eclipse的插件類(lèi)加載器,并分析一下插件環(huán)境下的類(lèi)加載和普通java應用場(chǎng)景中的類(lèi)加載有什么不同,并會(huì )提供一個(gè)比較完整的類(lèi)加載器。
插件類(lèi)加載器就是一個(gè)由eclipse開(kāi)發(fā)的一個(gè)用戶(hù)自定義類(lèi)加載器,所以分析時(shí)候用到的一些基本的東西都是在這篇文章中涉及到的。
這篇文章寫(xiě)的時(shí)候時(shí)間比較緊,亂糟糟的,大家見(jiàn)諒。中間參考了JVM規范、jdk文檔和代碼、《Inside Java Virtual Machine》一書(shū)等資料。
文章中肯定包含了一些錯誤,歡迎指出,謝謝!
聯(lián)系客服