類加載器,顧名思義,即是實現類加載的功能模塊,負責將Class的字節碼形式加載成內存形式的Class對象。字節碼文件可來源於磁盤或者jar包中的Class文件,也可以來自網絡字節流。
類加載器
在JVM中,內置了三個重要的類加載器,Application classLoader,Extension classLoader和Bootstrap classLoader。應用類加載器,擴展類加載器和啓動類加載器。
- Bootstrap classLoader啓動類加載器:加載JAVA_HOME/lib/rt.jar下的核心類,比如
java.util.*、java.io.*、java.nio.*、java.lang.*
等等。使用C代碼實現,Java無法訪問。 - Extension classLoader擴展類加載器:加載JAVA_HOME/lib/ext/*.jar中的擴展類,比如 swing 系列、內置的 js 引擎、xml 解析器 等等,這些庫名通常以 javax 開頭。
- Application classLoader應用類加載器:加載Classpath環境變量裏定義的路徑中的jar包和目錄。自己編寫的代碼和第三方jar都由該類加載器加載。
三種類加載器存在傳遞性。Application classLoader 加載類時,會先問問Extension classLoader是否加載過,會在再問問Bootstrap classLoader是否加載過。
每個Class對象裏面都有一個classLoader屬性記錄當前類由哪個類加載器加載
雙親委派模型
雙親委派機制也很好理解,AppClassLoader只負責加載ClassPath下的class文件,需要加載系統類庫時,會委託上級類加載器,BootstrapClassLoader和ExtensionClassLoader,去加載對應的類庫,這就是所謂的“雙親委派模型”
下面通過源碼來分析雙親委派的流程。此處的loadClass方法來源於類加載器抽象類ClassLoader。該方法是加載類的入口。用戶可以繼承ClassLoader來自定義類加載器。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);//檢查名稱爲name的類是否被本類加載器加載過
if (c == null) {// 爲null表示沒有被加載
long t0 = System.nanoTime();
try {
if (parent != null) {//上級不爲null
c = parent.loadClass(name, false);//遞歸調用上級類加載器loadClass方法
} else {//上級爲null,即表示上級是啓動類加載器
c = findBootstrapClassOrNull(name);//委託啓動類加載器在javaHome/jre/lib下尋找名稱爲name的類
}
} catch (ClassNotFoundException e) {
// 如果上級類加載器沒有找到名稱爲name的類,則在此處捕獲ClassNotFoundException異常
}
if (c == null) {//c爲null表示上級類加載器沒有找到name類
long t1 = System.nanoTime();
c = findClass(name);//到本加載器的路徑下尋找名稱爲name的類(例如擴展類加載器則是lib/ext/下)
// 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;
}
}
綜上,類加載器加載類的具體流程如下:
- 先檢查本類加載器是否加載類,如果已加載,結束,沒加載進入下一步;
- 遞歸委派上級類加載器;
- 如果沒有上級了,執行啓動類加載器,查找類是否存在對應的路徑下;
- 上級都沒有找到類時,跳出遞歸,在本類加載器對應路徑下查找類。
雙親委派的好處:根據雙親委派模型的特點,可以知道,越是基礎的類,由越上層的類加載器來加載,如此一來,Java類隨着類加載具備了一個帶有優先級地層次關係。這樣可以保證,若用戶編寫了與Java類庫中的類重名的類,此類不會被加載,因爲同名的類往往是會被委派給啓動類加載器或擴展類加載器來加載。因此雙親委派機制可以保證Java程序的穩定性
破壞雙親委派
在雙親委派機制中,我們知道,基礎的類由上級類加載器加載。雙親委派可以保證“基礎類作爲API被用戶代碼調用”這個場景能夠準確運行。但是,可能會存在一些基礎類調用用戶代碼的情況。例如,Java提供了很多服務提供者接口(Service Provider Interface, SPI),允許第三方爲這些接口提供實現,常見的SPI實現有JDBC、JCE、JNDI、JAXP等。SPI的接口由Java核心庫提供,而其實現代碼則是屬於應用程序的jar包(放進CLASSPATH中)。那麼問題來了,核心庫中的SPI的接口由啓動類加載來加載,CLASSPATH中的實現類由應用類加載器來加載,此種應用場景下,啓動類加載器是無法找到SPI的實現類的。
因此,需要通過某種特殊手段,來打破雙親委派,讓上級類加載器找不到類時,調用能獲取到目標類的下級類加載器來進行加載。jdk引入了“線程上下文類加載器”來解決此問題。
線程上下文類加載器
在Thread類中有一個成員變量,contextClassLoader
,如下所示
class Thread {
...
private ClassLoader contextClassLoader;
public ClassLoader getContextClassLoader() {
return contextClassLoader;
}
public void setContextClassLoader(ClassLoader cl) {
this.contextClassLoader = cl;
}
...
}
這個contextClassLoader
就是線程上下文類加載器,用於引用一個類加載器。可以通過setContextClassLoader
方法進行設置,若不設置,線程會從父線程中繼承一個類加載器。main線程的contextClassLoader
是應用類加載器,因此默認情況contextClassLoader
都指向AppClassLoader
。
按照雙親委派的機制,上級的BootStrapClassLoader
無法委派下級AppClassLoader
來加載類,但是可以通過線程中的contextClassLoader
來獲取到AppClassLoader
進行類加載。如此一來,便可以打破雙親委派的層次結構來逆向使用類加載器。