1.Java 類加載器
在Java中,類加載器是用來通過一個類的全限定名來獲取描述此類的二進制字節流的代碼模塊。
對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性。換句話說,比較兩個類是否相等,只有在這兩個類是被同一個類加載器加載時纔有意義,否則即使這兩個類來源於同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那麼這兩個類必定不相等。
下面我們用一個例子來了解一下被不同類加載器加載的同一個類會有什麼影響:
package com.wxueyuan.classloader;
import java.io.IOException;
import java.io.InputStream;
public class ClassLoaderTest {
public static void main(String[] args) throws Exception{
ClassLoaderTest instance = new ClassLoaderTest();
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
InputStream is = getClass().getResourceAsStream(fileName);
if(is == null) return super.loadClass(name);
byte[] b = null;
try {
b = new byte[is.available()];
is.read(b);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return defineClass(name, b, 0,b.length);
}
};
Object obj = myLoader.loadClass("com.wxueyuan.classloader.ClassLoaderTest").newInstance();
System.out.println(instance.getClass());
System.out.println(obj.getClass());
System.out.println(instance.getClass().equals(obj.getClass()));
System.out.println(instance instanceof com.wxueyuan.classloader.ClassLoaderTest);
System.out.println(obj instanceof com.wxueyuan.classloader.ClassLoaderTest);
System.out.println(instance.getClass().getClassLoader());
System.out.println(obj.getClass().getClassLoader());
}
}
這段程序的運行結果如下:
class com.wxueyuan.classloader.ClassLoaderTest
class com.wxueyuan.classloader.ClassLoaderTest
false
true
false
sun.misc.Launcher
從前兩句輸出我們可以看出,instance對象和obj對象都是class com.wxueyuan.classloader.ClassLoaderTest這個類的實例。
但是當我們嘗試instance.getClass().equals(obj.getClass())卻得到了false,這兩個實例的類並不相等,這就是因爲ClassLoaderTest這個類是由不同的類加載器加載的,換句話說在Java虛擬機中存在着兩個不同的ClassLoaderTest類。
當我們分別用instance和obj對象進行instanceof操作時我們發現instance返回true,而obj返回false;這是因爲默認的 com.wxueyuan.classloader.ClassLoaderTest這個類是由系統應用程序類加載器加載的,而obj這個實例其實是我們用myLoader類加載器加載的類的實例。最後兩行打印輸出了這兩個對象所屬的不同的類加載器。
2. 類加載器的分類
從Java虛擬機的角度來看,類加載器可以分爲兩類:
- 啓動類加載器(Bootstrap ClassLoader): 這個類加載器是由C++實現的,是Java虛擬機的一部分。
- 其它類加載器: 除了啓動類加載器,其它所有的加載器都是由Java實現的,獨立於Java虛擬機外部,都繼承自抽象類Java.lang.ClassLoader。
從Java開發人員的角度來看,類加載器可以分爲四類:
- 啓動類加載器(Bootstrap ClassLoader): 這個類加載器主要負責加載JAVA_HOME/lib目錄下,或者被-Xbootclasspath參數所指定的路徑下的,可以被虛擬機識別的指定格式的(如rt.jar)類庫。
- 擴展類加載器(Extension ClassLoader): 這個類加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載JAVA_HOME/lib/ext目錄下的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫。
- 應用程序類加載器(Application ClassLoader): 這個類加載器由sun.misc.Launcher
AppClassLoader實現。這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以我們也稱它爲系統類加載器。它負責加載用戶類路徑上所指定的類庫,如果應用程序沒有自定義過類加載器,這個類加載器也是默認的類加載器,現在大家知道上面的例子當中爲什麼instance實例的getClassLoader()方法返回的是sun.misc.Launcher AppClassLoader@73d16e93了吧。 - 自定義類加載器:通常情況下我們的應用程序就是由這三種類加載器配合加載的,但是在必要的情況下我們可以自定義類加載器,就比如我們上面的myLoader。
3. 雙親委派模型
上面介紹的4中類加載器之間存在着一種層次關係,這種層次關係也被稱爲雙親委派模型(Parents Delegastion Model),如下圖所示:
雙親委派模型要求除了頂層的啓動類加載器之外,其它所有的類加載器都必須有自己的父加載器。當一個類加載器收到了類加載的請求之後,它首先不會嘗試自己加載這個類,而是將這個請求委派給父類加載器去完成,由於每一個類加載器都有這個邏輯,因此所有的類加載請求最終都應該傳送到頂層的啓動類加載器中。只有在父加載器在它的搜索範圍內沒有找到所需的類時,子加載器纔會嘗試去加載。
雙親委派模型對Java程序的穩定運行很重要,因爲即使我們創建了與類庫中全限定名相同的類,我們的類也不會被加載而影響到整個Java的運行環境。它的實現邏輯都集中在java.lang.ClassLoader.loadClass()方法中
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先檢查請求的類是否已經被加載過了
Class<?> c = findLoadedClass(name);
//如果該類沒有被加載過
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//調用父類加載器的loadClass
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();
//在父類加載器無法加載的時候,再調用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) {
resolveClass(c);
}
return c;
}
}
4. 破壞雙親委派模型
儘管雙親委派模型對Java程序的運行十分重要,但是在一些情況下,我們不得不對雙親委派模型進行破壞。比如雙親委派模型能夠保證越基礎的類越由上層的加載器加載,基礎的類庫之所以基礎,是因爲它們提供了大量的用戶需要調用的API,但是在一些情況下基礎的類庫有可能要反過來調用用戶提供的代碼又該怎麼辦呢?
有的同學可能會說哪有基礎類庫需要調用用戶提供的代碼的時候,但事實上這種情況確實存在。比如SUN公司提供JNDI服務用來對命名服務或目錄服務的資源進行管理和查找,它的代碼是由啓動類加載器去加載的(rt.jar),但是它需要調用由服務供應商提供的SPI的實現類(JNDI相關知識請關注此處)。
爲了解決這個問題,JAVA設計團隊提供了線程上下文類加載器(Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設置,如果創建線程時未設置,則會繼承父線程的ContextClassLoader。
有了線程上下文類加載器,JAVA就可以實現父類加載器請求子類加載器去完成某些類的加載動作,這種行爲相當於逆向使用了雙親委派模型的加載順序。
5. 總結
在這篇博客中博主與大家一起學習了一下Java中的類加載機制,以及完成類加載動作的類加載器和傳統的雙親委派模型,事實上,類加載的過程包括着“加載”,“驗證”,“準備”,“解析”和“初始化”等5個階段,博主在這裏就不贅述了,感興趣的同學可以自己下去查些資料去更深一步地瞭解類加載的過程。