深入解析Java中的Class Loader類加載器
類加載的過程
類加載器的主要工作就是把類文件加載到JVM中。如下圖所示,其過程分為三步:
1.加載:定位要加載的類文件,并將其字節(jié)流裝載到JVM中;
2.鏈接:給要加載的類分配最基本的內(nèi)存結(jié)構保存其信息,比如屬性,方法以及引用的類。在該階段,該類還處于不可用狀態(tài);
(1)驗證:對加載的字節(jié)流進行驗證,比如格式上的,安全方面的;
(2)內(nèi)存分配:為該類準備內(nèi)存空間來表示其屬性,方法以及引用的類;
(3)解析:加載該類所引用的其它類,比如父類,實現(xiàn)的接口等。
3.初始化:對類變量進行賦值。

類加載器的層級
下圖虛線以上是JDK提供的幾個重要的類加載器,詳細說明如下:
(1)Bootstrap Class Loader: 當啟動包含主函數(shù)的類時,加載JAVA_HOME/lib目錄下或-Xbootclasspath指定目錄的jar包;
(2)Extention Class Loader:加載JAVA_HOME/lib/ext目錄下的或-Djava.ext.dirs指定目錄下的jar包。
(3)System Class Loader:加載classpath或者-Djava.class.path指定目錄下的類或jar包。

需要了解的是:
1.除了Bootstrap Class Loader外,其它的類加載器都是java.lang.ClassLoader類的子類;
2.Bootstrap Class Loader不是用Java實現(xiàn),如果你沒有使用個性化類加載器,那么java.lang.String.class.getClassLoader()就為null,Extension Class Loader的父加載器也為null;
3.獲得類加載器的幾種方式:
(1)獲得Bootstrap Class Loader:試圖獲得Bootstrap Class Loader,得到的必然是null??梢杂萌缦路绞津炞C下:使用rt.jar包內(nèi)的類對象的getClassLoader方法,比如java.lang.String.class.getClassLoader()可以得到或者獲得Extention Class Loader,再調(diào)用getParent方法獲得;
(2)獲得Extention Class Loader:使用JAVA_HOME/lib/ext目錄下jar包內(nèi)的類對象的getClassLoader方法或者先獲得System Class Loader,再通過它的getParent方法獲得;
(3)獲得System Class Loader:調(diào)用包含主函數(shù)的類對象的getClassLoader方法或者在主函數(shù)內(nèi)調(diào)用Thread.currentThread().getContextClassLoader()或者調(diào)用ClassLoader.getSystemClassLoader();
(4)獲得User-Defined Class Loader:調(diào)用類對象的getClassLoader方法或者調(diào)用Thread.currentThread().getContextClassLoader();
類加載器的操作原則
1.代理原則
2.可見性原則
3.唯一性原則
4.代理原則
代理原則指的是一個類加載器在加載一個類時會請求它的父加載器代理加載,父加載器也會請求它的父加載器代理加載,如下圖所示。

為什么要使用代理模式呢?首先這樣可以減少重復的加載一個類。(還有其它原因嗎?)
容易誤解的地方:
一般會以為類加載器的代理順序是Parent First的,也就是:
1.加載一個類時,類加載器首先檢查自己是否已經(jīng)加載了該類,如果已加載,則返回;否則請父加載器代理;
2.父加載器重復1的操作一直到Bootstrap Class Loader;
3.如果Bootstrap Class Loader也沒有加載該類,將嘗試進行加載,加載成功則返回;如果失敗,拋出ClassNotFoundException,則由子加載器進行加載;
4.子類加載器捕捉異常后嘗試加載,如果成功則返回,如果失敗則拋出ClassNotFoundException,直到發(fā)起加載的子類加載器。
這種理解對Bootstrap Class Loader,Extention Class Loader,System Class Loader這些加載器是正確的,但一些個性化的加載器則不然,比如,IBM Web Sphere Portal Server實現(xiàn)的一些類加載器就是Parent Last的,是子加載器首先嘗試加載,如果加載失敗才會請父加載器,這樣做的原因是:假如你期望某個版本log4j被所有應用使用,就把它放在WAS_HOME的庫里,WAS啟動時會加載它。如果某個應用想使用另外一個版本的log4j,如果使用Parent First,這是無法實現(xiàn)的,因為父加載器里已經(jīng)加載了log4j內(nèi)的類。但如果使用Parent Last,負責加載應用的類加載器會優(yōu)先加載另外一個版本的log4j。
可見性原則
每個類對類加載器的可見性是不一樣的,如下圖所示。
擴展知識,OSGi就是利用這個特點,每一個bundle由一個單獨的類加載器加載,因此每個類加載器都可以加載某個類的一個版本,因此整個系統(tǒng)就可以使用一個類的多個版本。

唯一性原則
每一個類在一個加載器里最多加載一次。
擴展知識1:準確地講,Singleton模式所指的單例指的是在一組類加載器中某個類的對象只有一份。
擴展知識2:一個類可以被多個類加載器加載,每個類對象在各自的namespace內(nèi),對類對象進行比較或者對實例進行類型轉(zhuǎn)換時,會同時比較各自的名字空間,比如:
Klass類被ClassLoaderA加載,假設類對象為KlassA;同時被ClassLoaderB加載,假設類對象為KlassB,那么KlassA不等于KlassB。同時ClassA的實例被cast成KlassB時會拋出ClassCastException異常。
為什么要個性化類加載器
個性化類加載器給Java語言增加了很多靈活性,主要的用途有:
1.可以從多個地方加載類,比如網(wǎng)絡上,數(shù)據(jù)庫中,甚至即時的編譯源文件獲得類文件;
2.個性化后類加載器可以在運行時原則性的加載某個版本的類文件;
3.個性化后類加載器可以動態(tài)卸載一些類;
4.個性化后類加載器可以對類進行解密解壓縮后再載入類。
類的隱式和顯式加載
隱式加載:當一個類被引用,被繼承或者被實例化時會被隱式加載,如果加載失敗,是拋出NoClassDefFoundError。
顯式加載:使用如下方法,如果加載失敗會拋出ClassNotFoundException。
cl.loadClass(),cl是類加載器的一個實例;
Class.forName(),使用當前類的類加載器進行加載。
類的靜態(tài)塊的執(zhí)行
假如有如下類:
package cn.fengd;
public class Dummy {
static {
System.out.println("Hi");
}
}
另建一個測試類:
package cn.fengd;
public class ClassLoaderTest {
public static void main(String[] args) throws InstantiationException, Exception {
try {
/*
* Different ways of loading.
*/
Class c = ClassLoaderTest.class.getClassLoader().loadClass("cn.fengd.Dummy");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
運行后效果如何呢?
- 不會輸出Hi。由此可見使用loadClass后Class類對象并沒有初始化;
- 如果在Load語句后加上c.newInstance(); 就會有Hi輸出,對該類進行實例化時才初始化類對象。
- 如果換一種加載語句Class c = Class.forName("cn.fengd.Dummy", false, ClassLoader.getSystemClassLoader());
- 不會輸出Hi。因為參數(shù)false表示不需要初始化該類對象;
- 如果在Load語句后加上c.newInstance(); 就會有Hi輸出,對該類進行實例化時才初始化類對象。
如果換成Class.forName("cn.fengd.Dummy");或者new Dummy()呢?
都會輸出Hi。
常見問題分析:
1.由不同的類加載器加載的指定類型還是相同的類型嗎?
在Java中,一個類用其完全匹配類名(fully qualified class name)作為標識,這里指的完全匹配類名包括包名和類名。但在JVM中一個類用其全名和一個加載類ClassLoader的實例作為唯一標識,不同類加載器加載的類將被置于不同的命名空間.我們可以用兩個自定義類加載器去加載某自定義類型(注意,不要將自定義類型的字節(jié)碼放置到系統(tǒng)路徑或者擴展路徑中,否則會被系統(tǒng)類加載器或擴展類加載器搶先加載),然后用獲取到的兩個Class實例進行java.lang.Object.equals(…)判斷,將會得到不相等的結(jié)果。這個大家可以寫兩個自定義的類加載器去加載相同的自定義類型,然后做個判斷;同時,可以測試加載java.*類型,然后再對比測試一下測試結(jié)果。
2.在代碼中直接調(diào)用Class.forName(String name)方法,到底會觸發(fā)那個類加載器進行類加載行為?
Class.forName(String name)默認會使用調(diào)用類的類加載器來進行類加載。我們直接來分析一下對應的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() {
// 獲取調(diào)用類(caller)的類型
Class caller = Reflection.getCallerClass(3);
// This can be null if the VM is requesting it
if (caller == null) {
returnnull;
}
// 調(diào)用java.lang.Class中本地方法獲取加載該調(diào)用類(caller)的ClassLoader
return caller.getClassLoader0();
}
//java.lang.Class.java
//虛擬機本地實現(xiàn),獲取當前類的類加載器
native ClassLoader getClassLoader0();
3.在編寫自定義類加載器時,如果沒有設定父加載器,那么父加載器是?
在不指定父類加載器的情況下,默認采用系統(tǒng)類加載器。可能有人覺得不明白,現(xiàn)在我們來看一下JDK對應的代碼實現(xiàn)。眾所周知,我們編寫自定義的類加載器直接或者間接繼承自java.lang.ClassLoader抽象類,對應的無參默認構造函數(shù)實現(xiàn)如下:
//摘自java.lang.ClassLoader.java
protected ClassLoader() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
this.parent = getSystemClassLoader();
initialized = true;
}
我們再來看一下對應的getSystemClassLoader()方法的實現(xiàn):
privatestaticsynchronizedvoid initSystemClassLoader() {
//...
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
scl = l.getClassLoader();
//...
}
我們可以寫簡單的測試代碼來測試一下:
System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());
本機對應輸出如下:
sun.misc.Launcher$AppClassLoader@197d257
所以,我們現(xiàn)在可以相信當自定義類加載器沒有指定父類加載器的情況下,默認的父類加載器即為系統(tǒng)類加載器。同時,我們可以得出如下結(jié)論:
即時用戶自定義類加載器不指定父類加載器,那么,同樣可以加載如下三個地方的類:
(1)<Java_Runtime_Home>/lib下的類
(2)< Java_Runtime_Home >/lib/ext下或者由系統(tǒng)變量java.ext.dir指定位置中的類
(3)當前工程類路徑下或者由系統(tǒng)變量java.class.path指定位置中的類
4.在編寫自定義類加載器時,如果將父類加載器強制設置為null,那么會有什么影響?如果自定義的類加載器不能加載指定類,就肯定會加載失敗嗎?
JVM規(guī)范中規(guī)定如果用戶自定義的類加載器將父類加載器強制設置為null,那么會自動將啟動類加載器設置為當前用戶自定義類加載器的父類加載器(這個問題前面已經(jīng)分析過了)。同時,我們可以得出如下結(jié)論:
即時用戶自定義類加載器不指定父類加載器,那么,同樣可以加載到<Java_Runtime_Home>/lib下的類,但此時就不能夠加載<Java_Runtime_Home>/lib/ext目錄下的類了。
說明:問題3和問題4的推斷結(jié)論是基于用戶自定義的類加載器本身延續(xù)了java.lang.ClassLoader.loadClass(…)默認委派邏輯,如果用戶對這一默認委派邏輯進行了改變,以上推斷結(jié)論就不一定成立了,詳見問題5。
5.編寫自定義類加載器時,一般有哪些注意點?
(1)一般盡量不要覆寫已有的loadClass(…)方法中的委派邏輯
一般在JDK 1.2之前的版本才這樣做,而且事實證明,這樣做極有可能引起系統(tǒng)默認的類加載器不能正常工作。在JVM規(guī)范和JDK文檔中(1.2或者以后版本中),都沒有建議用戶覆寫loadClass(…)方法,相比而言,明確提示開發(fā)者在開發(fā)自定義的類加載器時覆寫findClass(…)邏輯。舉一個例子來驗證該問題:
//用戶自定義類加載器WrongClassLoader.Java(覆寫loadClass邏輯)
publicclassWrongClassLoaderextends ClassLoader {
public Class<?> loadClass(String name) throws ClassNotFoundException {
returnthis.findClass(name);
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
//假設此處只是到工程以外的特定目錄D:/library下去加載類
具體實現(xiàn)代碼省略
}
}
通過前面的分析我們已經(jīng)知道,用戶自定義類加載器(WrongClassLoader)的默
認的類加載器是系統(tǒng)類加載器,但是現(xiàn)在問題4種的結(jié)論就不成立了。大家可以簡
單測試一下,現(xiàn)在<Java_Runtime_Home>/lib、< Java_Runtime_Home >/lib/ext和工
程類路徑上的類都加載不上了。
問題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();
}
}
}
(說明:D:"classes"beans"Account.class物理存在的)
輸出結(jié)果:
java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系統(tǒng)找不到指定的路徑。) 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)
這說明,連要加載的類型的超類型java.lang.Object都加載不到了。這里列舉的由于覆寫loadClass(…)引起的邏輯錯誤明顯是比較簡單的,實際引起的邏輯錯誤可能復雜的多。
問題5測試二
//用戶自定義類加載器WrongClassLoader.Java(不覆寫loadClass邏輯)
publicclassWrongClassLoaderextends ClassLoader {
protected Class<?> findClass(String name) throws ClassNotFoundException {
//假設此處只是到工程以外的特定目錄D:/library下去加載類
具體實現(xiàn)代碼省略
}
}
將自定義類加載器代碼WrongClassLoader.Java做以上修改后,再運行測試代碼,輸出結(jié)果如下:
beans.Account WrongClassLoader@1c78e57
這說明,beans.Account加載成功,且是由自定義類加載器WrongClassLoader加載。
這其中的原因分析,我想這里就不必解釋了,大家應該可以分析的出來了。
(2)正確設置父類加載器
通過上面問題4和問題5的分析我們應該已經(jīng)理解,個人覺得這是自定義用戶類加載器時最重要的一點,但常常被忽略或者輕易帶過。有了前面JDK代碼的分析作為基礎,我想現(xiàn)在大家都可以隨便舉出例子了。
(3)保證findClass(String )方法的邏輯正確性
事先盡量準確理解待定義的類加載器要完成的加載任務,確保最大程度上能夠獲取到對應的字節(jié)碼內(nèi)容。
6.如何在運行時判斷系統(tǒng)類加載器能加載哪些路徑下的類?
一是可以直接調(diào)用ClassLoader.getSystemClassLoader()或者其他方式獲取到系統(tǒng)類加載器(系統(tǒng)類加載器和擴展類加載器本身都派生自URLClassLoader),調(diào)用URLClassLoader中的getURLs()方法可以獲取到;
二是可以直接通過獲取系統(tǒng)屬性java.class.path 來查看當前類路徑上的條目信息 , System.getProperty("java.class.path")
7.如何在運行時判斷標準擴展類加載器能加載哪些路徑下的類?
方法之一:
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
相關文章
Java操作redis實現(xiàn)增刪查改功能的方法示例
這篇文章主要介紹了Java操作redis實現(xiàn)增刪查改功能的方法,涉及java操作redis數(shù)據(jù)庫的連接、設置、增刪改查、釋放資源等相關操作技巧,需要的朋友可以參考下2017-08-08
解決FontConfiguration.getVersion報空指針異常的問題
這篇文章主要介紹了解決FontConfiguration.getVersion報空指針異常的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-06-06
java 使用線程監(jiān)控文件目錄變化的實現(xiàn)方法
這篇文章主要介紹了java 使用線程監(jiān)控文件目錄變化的實現(xiàn)方法的相關資料,希望通過本文能幫助到大家,需要的朋友可以參考下2017-10-10

