類加載器簡介
類加載器負責在運行時將Java class動態(tài)加載到JVM(Java虛擬機)。而且,它們是JRE(Java運行時環(huán)境)的一部分。 因此,由于類加載器的緣故,JVM無需了解底層文件或文件系統(tǒng)即可運行Java程序。
而且,這些Java class不會一次全部加載到內(nèi)存中,而是在應用程序需要時加載。 這是類加載器發(fā)揮作用的時候, 他們負責將類加載到內(nèi)存中。
在本篇內(nèi)容中,我們將討論不同類型的內(nèi)置類加載器,它們?nèi)绾喂ぷ饕约白远x實現(xiàn)的介紹。
內(nèi)置的類加載器類型
讓我們從一個簡單的示例開始學習如何使用各種類加載器加載不同的類:
public class ClassLoaderTest {
@Test
public void printClassLoaders() throws ClassNotFoundException {
System.out.println("Classloader of this class:" + ClassLoaderTest.class.getClassLoader());
System.out.println("Classloader of Logging:" + Logging.class.getClassLoader());
System.out.println("Classloader of ArrayList:" + ArrayList.class.getClassLoader());
}
}
// console output
Classloader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2
Classloader of Logging:sun.misc.Launcher$ExtClassLoader@65b54208
Classloader of ArrayList:null
如我們所見,這里有三種不同的類加載器: AppClassLoader,ExtClassLoader和BootstrapClassLoader (顯示為null)。
AppClassLoader 加載示例方法的類. 應用或系統(tǒng)類加載器加載classpath下的類文件.
接下來, ExtClassLoader加載 Logging class. ExtensionClassLoaders 加載作為標準核心Java類的擴展的類.
但是, 我們看到最后一個的輸出, 對于ArrayList,輸出顯示為 null . 這是因為BootstrapClassLoader 是用native code編寫的, 而非Java – 因此它不會顯示為Java class. 由于這個原因,引導類加載器的行為在不同JVM之間會有所不同。
總體而言,下圖顯示了不同類加載器之間的聯(lián)系和區(qū)別:

接下來我們詳細討論這些類加載器。
Bootstrap Class Loader
Java classes 由 java.lang.ClassLoader的實例加載. 但是, ClassLoaders 它們本身也是類. 那么問題來了, 誰來加載 java.lang.ClassLoader 本身呢?
這正是圖中引導程序或原始類裝載器應用的地方。
它主要負責加載JDK內(nèi)部類, 通常是 rt.jar 和$JAVA_HOME/jre/lib 目錄中的其他核心庫. 此外, Bootstrap 類加載器還充當其他 ClassLoader instances的父類.
Bootstrap ClassLoader 是 JVM 核心的重要內(nèi)容部分,它是用Native Code編寫的 . 不同的平臺( HotSpot 、 JRockit 、 J9 )可能對它有不同的實現(xiàn)方式.
Extension Class Loader
Extension ClassLoader 是Bootstrap ClassLoader的子類,負責加載標準核心 Java 的拓展類, 以便在平臺上運行整個應用程序.
Extension ClassLoader 從 JDK 拓展目錄, 通常是$JAVA_HOME/lib/ext 目錄或者 java.ext.dirs 系統(tǒng)屬性設置的目錄.
System Class Loader
另一方面,System ClassLoader(有的稱之為Application ClassLoader)負責加載應用級別的classes 到 JVM 中. 它負責加載classpath 環(huán)境變量下的文件, -classpath 或 -cp 命令行選項中的文件. 而且,它是Extension ClassLoader的子類。
ClassLoader如何工作?
ClassLoaders 是 Java運行環(huán)境的一部分. JVM 請求一個類時, ClassLoader將使用完全限定名去定位該類,并加載到jvm運行時環(huán)境中.
java.lang.ClassLoader.loadClass() 方法負責將類定義加載到運行時. 它嘗試使用完全限定名去加載類.
如果該類尚未被加載, 它將委托父類加載器去加載. 這是一個遞歸過程.
最終, 如果父類加載器沒有找到該類, 則子類將調(diào)用 java.net.URLClassLoader.findClass() 方法在自己的文件系統(tǒng)中尋找該類.
如果最后一個子類也無法加載該類, 則會拋出java.lang.NoClassDefFoundError 或java.lang.ClassNotFoundException.
讓我們看一個拋出ClassNotFoundException的輸出示例。
Exception in thread "main" java.lang.ClassNotFoundException: com.bern.classloader.SampleClassLoader
at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:264)
如果我們從調(diào)用java.lang.Class.forName()開始查看事件軸,我們發(fā)現(xiàn),它首先艙室通過父類加載器加載該類,然后通過java.net.URLClassLoader.findClass() 查找類本身. 當依舊無法找到該類,它將拋出 ClassNotFoundException
類加載器有三個重要的特性。
委托模型
ClassLoaders 遵循委托模型,在該模型中,根據(jù)請求查找class 或者 resource, ClassLoader實例將會委托給父類加載器去尋找class或者resource.
假設我們需要將應用程序類加載到JVM中的請求。 AppClassLoader首先將該類的加載委托給其父類Extension ClassLoader,而Extension ClassLoader又將其委托給Bootstrap ClassLoader。
僅當Bootstrap 和 Extension ClassLoader 未能成功加載該類時, Application ClassLoader才會艙室加載該類.
唯一性
作為委托模型的結果,很容易確保 唯一的類,因為我們總是嘗試向上委托. 如果父類加載器沒有找到該類, 則當前的實例才會去嘗試尋找該類.
可見性
另外, 子ClassLoaders 對其父ClassLoaders加載的類可見.例如System ClassLoader加載的類對Extension ClassLoader和Bootstrap ClassLoader加載的類可見,反過來卻不可以。
為了說明這一點, 如果Class A 是由Application ClassLoader加載的, class B 是由Extensions ClassLoader加載的, 則就Application ClassLoader加載的其他類而言 ,A 和 B 都是可見的.
但是,就Extensions ClassLoader加載的其他類而言,類B是唯一可見的類。
Custom ClassLoader
在大多數(shù)情況下,內(nèi)置的類加載器就足夠了。 但是,在需要從本地硬盤驅(qū)動器以外或網(wǎng)絡中加載類的情況下,我們可能需要使用自定義類加載器。接下來,我們將介紹自定義類加載器的一些用例場景,并創(chuàng)建一個示例。
Custom Class Loaders Use-Cases
自定義類加載器不僅在運行時加載類有用,還包括如下一些使用場景:
1、幫助修改現(xiàn)有的字節(jié)碼,例如 weaving agents 。
2、動態(tài)創(chuàng)建適合用戶需求的類。 例如在JDBC中,通過動態(tài)類加載實現(xiàn)不同驅(qū)動程序之間的切換。
3、在具有相同名稱和包的類的情況下,通過加載不同的字節(jié)碼實現(xiàn)版本控制機制。這可以通過URL類加載器(通過URL加載jar)或自定義類加載器來完成。
在更具體的示例中,自定義類加載器可能會派上用場。 例如,瀏覽器使用自定義類加載器從網(wǎng)絡加載可執(zhí)行內(nèi)容. 瀏覽器可以使用單獨的類加載器從不同的網(wǎng)頁加載applet. 用于運行applets 的 applet viewer包含一個ClassLoader, 該ClassLoader可訪問遠程服務器上的網(wǎng)站,而無需在本地文件系統(tǒng)查找內(nèi)容。然后通過HTTP加載原始字節(jié)碼文件,并將其裝載到JVM中. 即使這些applets具有相同的名稱, 但如果由不同的類加載器加載,它們也被視為不同的組件. 現(xiàn)在,我們已經(jīng)對自定義ClassLoader的使用場景有了一些了解,接下來讓我們通過一個自定義 ClassLoader 深入的理解JVM如何加載classes.
Creating our Custom Class Loader
下面的示例創(chuàng)建一個自定義的ClassLoader ,并從文件中讀取class加載到JVM中, 我們需要繼承ClassLoader類并重寫 findClass()方法:
package com.bern.classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
public class CustomClassLoader extends ClassLoader {
@Override
public Class findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassFromFile(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassFromFile(String fileName) {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(
fileName.replace('.', File.separatorChar) + ".class");
byte[] buffer;
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
int nextValue = 0;
try {
while ( (nextValue = inputStream.read()) != -1 ) {
byteStream.write(nextValue);
}
} catch (IOException e) {
e.printStackTrace();
}
buffer = byteStream.toByteArray();
return buffer;
}
}
深入理解java.lang.ClassLoader
我們接下來了解以下 java.lang.ClassLoader 類中的一些重要方法,以便能夠更好的理解它的工作方式.
loadClass() 方法
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
此方法負責加載給定name參數(shù)的類. name 參數(shù)必須是類的完全限定名. 如果resolve 參數(shù)設置為true, 則Java虛擬機調(diào)用loadClass() 方法來解析class 引用. 但是, 并不一定非要解析一個類. 如果我們只想確定該類是否存在,而無需去解析, 只需要將參數(shù) resolve 設置為false即可.** 此方法是類加載器的入口點。 我們可以通過查看java.lang.ClassLoader 的源碼來了解loadClass()的工作原理:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
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();
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;
}
}
該方法的默認實現(xiàn)按以下順序搜索類:
- 調(diào)用findLoadedClass(String) 方法查看該類是否已經(jīng)被加載.
- 調(diào)用父類加載器的 loadClass(String) 方法查找該類.
- 如果父類加載器沒有找到,則調(diào)用 findClass(String) 方法查找該類.
defineClass() 方法
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError
此方法負責將字節(jié)數(shù)組轉(zhuǎn)換為類的實例. 在使用該類之前, 我們需要先對它進行解析. 如果類的格式不正確, 則會拋出一個ClassFormatError異常. 另外, 因為該方法時final類型,所以我們無法對它進行重寫.
findClass() 方法
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name);}
該方法使用完全限定名作為參數(shù)來查找class. 我們需要在自定義ClassLoader中重寫該方法. 如果父ClassLoader找不到請求的類,則 loadClass() 會調(diào)用此方法。 如果所有的父ClassLoader沒有找到該類,則默認實現(xiàn)將會拋出 ClassNotFoundException 異常.
getParent() 方法
public final ClassLoader getParent()
此方法返回父類加載器進行委派. 有些JVM實現(xiàn)(如第二部分)使用null表示Bootstrap ClassLoader.
getResource() 方法
public URL getResource(String name) {
此方法用來查找給定名稱的資源(資源的名稱是用 / 分隔的路徑名).
它首先委托父加載器查找資源. 如果 parent 返回null, 則搜索虛擬機內(nèi)置的類加載器的路徑. ** 如果都失敗了, 則該方法將調(diào)用 findResource(String) 來查找資源. 資源名稱相對于classpath來說可以是相對路徑,也可以是絕對路徑. 它通過讀取資源,返回一個URL 對象, 如果沒有找到該資源,或者沒有權限讀取該資源,則返回null. 特別需要要注意的是,Java是從 classpath 加載資源。 最后, Java中的資源加載被認為是與位置無關的 因為只是根據(jù)設置的環(huán)境變量來查找資源,代碼在何處運行都無關緊要.
Context Classloaders
通常, Context ClassLoaders 為 J2SE引入類加載委托方案提供了一種替代方法. 就像我們之前所學的, JVM中的類加載器遵循分層模型,因此除了Bootstrap ClassLoader,每個類加載器都有且僅有一個父級. 但是, 有時當JVM核心類需要動態(tài)加載應用程序級別的類或資源時, 我們可能會遇到一些問題. 例如, 在JNDI中,核心功能由 rt.jar 中的Bootstrap Classes 實現(xiàn). 但是這些 JNDI classes 可能會加載由 JNDI 獨立供應商提供的JNDI的實現(xiàn) (部署在應用的 classpath). 這種情況要求Bootstrap ClassLoader (父類加載器) 加載的類對Application ClassLoader (子類加載器)可見. J2SE 委托模型不能解決這種問題,為了解決這個問題, 我們需要找到替代的類加載方式. 我們可以使用線程上下文加載器來應對這種場景. java.lang.Thread 類有一個 getContextClassLoader()方法,該方法返回特定線程的ContextClassLoader . 當加載類或資源時,線程的創(chuàng)建者提供ContextClassLoader . 如果沒有設置上下文加載器, 默認是父線程的上下文加載器.
類加載器與 OSGi
OSGi?是 Java 上的動態(tài)模塊系統(tǒng)。它為開發(fā)人員提供了面向服務和基于組件的運行環(huán)境,并提供標準的方式用來管理軟件的生命周期。OSGi 已經(jīng)被實現(xiàn)和部署在很多產(chǎn)品上,在開源社區(qū)也得到了廣泛的支持。Eclipse 就是基于 OSGi 技術來構建的。
OSGi 中的每個模塊(bundle)都包含 Java 包和類。模塊可以聲明它所依賴的需要導入(import)的其它模塊的 Java 包和類(通過 Import-Package),也可以聲明導出(export)自己的包和類,供其它模塊使用(通過 Export-Package)。也就是說需要能夠隱藏和共享一個模塊中的某些 Java 包和類。這是通過 OSGi 特有的類加載器機制來實現(xiàn)的。OSGi 中的每個模塊都有對應的一個類加載器。它負責加載模塊自己包含的 Java 包和類。當它需要加載 Java 核心庫的類時(以 java開頭的包和類),它會代理給父類加載器(通常是啟動類加載器)來完成。當它需要加載所導入的 Java 類時,它會代理給導出此 Java 類的模塊來完成加載。模塊也可以顯式的聲明某些 Java 包和類,必須由父類加載器來加載。只需要設置系統(tǒng)屬性org.osgi.framework.bootdelegation的值即可。
假設有兩個模塊 bundleA 和 bundleB,它們都有自己對應的類加載器 classLoaderA 和 classLoaderB。在 bundleA 中包含類com.bundleA.Sample,并且該類被聲明為導出的,也就是說可以被其它模塊所使用的。bundleB 聲明了導入 bundleA 提供的類 com.bundleA.Sample,并包含一個類 com.bundleB.NewSample繼承自 com.bundleA.Sample。在 bundleB 啟動的時候,其類加載器 classLoaderB 需要加載類 com.bundleB.NewSample,進而需要加載類 com.bundleA.Sample。由于 bundleB 聲明了類 com.bundleA.Sample是導入的,classLoaderB 把加載類 com.bundleA.Sample的工作代理給導出該類的 bundleA 的類加載器 classLoaderA。classLoaderA 在其模塊內(nèi)部查找類 com.bundleA.Sample并定義它,所得到的類 com.bundleA.Sample實例就可以被所有聲明導入了此類的模塊使用。對于以 java開頭的類,都是由父類加載器來加載的。如果聲明了系統(tǒng)屬性org.osgi.framework.bootdelegation=com.example.core.*,那么對于包 com.example.core中的類,都是由父類加載器來完成的。
OSGi 模塊的這種類加載器結構,使得一個類的不同版本可以共存在 Java 虛擬機中,帶來了很大的靈活性。不過它的這種不同,也會給開發(fā)人員帶來一些麻煩,尤其當模塊需要使用第三方提供的庫的時候。下面提供幾條比較好的建議:
如果一個類庫只有一個模塊使用,把該類庫的 jar 包放在模塊中,在
Bundle-ClassPath中指明即可。如果一個類庫被多個模塊共用,可以為這個類庫單獨的創(chuàng)建一個模塊,把其它模塊需要用到的 Java 包聲明為導出的。其它模塊聲明導入這些類。
如果類庫提供了 SPI 接口,并且利用線程上下文類加載器來加載 SPI 實現(xiàn)的 Java 類,有可能會找不到 Java 類。如果出現(xiàn)了
NoClassDefFoundError異常,首先檢查當前線程的上下文類加載器是否正確。通過Thread.currentThread().getContextClassLoader()就可以得到該類加載器。該類加載器應該是該模塊對應的類加載器。如果不是的話,可以首先通過class.getClassLoader()來得到模塊對應的類加載器,再通過Thread.currentThread().setContextClassLoader()來設置當前線程的上下文類加載器。
總結
ClassLoaders對于執(zhí)行Java程序至關重要. 本文詳細的講解了相關的知識. 我們具體的討論了不同類型的加載器 – Bootstrap, Extensions 和 System ClassLoaders. Bootstrap ClassLoader是所有ClassLoader的父級,并負責加載 JDK 內(nèi)部 classes. Extensions 和 system, 分別加載從 Java 拓展目錄和 classpath 加載類. 然后,我們討論了類加載器的工作原理,并討論了一些它們的特性,如委托模型,可見性,唯一性. 然后講解了如何創(chuàng)建一個自定義的ClassLoader. 最后, we provided an introduction to Context class loaders. 然后,我們討論了類加載器的工作方式,并討論了一些功能,例如委托,可見性和唯一性,然后簡要說明了如何創(chuàng)建自定義的。 然后,我們介紹了上下文類加載器。最后,我們介紹了osgi和類加載器的關系。
所有代碼都已經(jīng)上傳至 GitHub.