在上一章里,我們已經學習了類加載的過程,我們知道在加載階段需要”通過一個類的全限定名來獲取描述該類的二進制字節流“,而來完成這個工作的就是類加載器(Class Loader)。
1、類與類加載器
類加載器只用於實現類的加載動作。
但對於任意一個類,都必須由加載它的類加載器和這個類本身一起共同確立其在Java虛擬機中的唯一性,每 一個類加載器,都擁有一個獨立的類名稱空間。
這句話可以表達得更通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源於同一個Class文件,被同一個Java虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。
如下演示了不同的類加載器對instanceof關鍵字運算的結果的影響。
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
//自定義一個簡單的類加載器
ClassLoader myLoader = new ClassLoader() {
@Override
//加載類方法
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
//獲取文件名
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
//加載輸入流
InputStream is = getClass().getResourceAsStream(fileName);
//使用父類加載
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
//從流中轉化類的實例
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
//使用自己實現的類加載器加載
Object obj = myLoader.loadClass("cn.fighter3.loader.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
//實例判斷
System.out.println(obj instanceof cn.fighter3.loader.ClassLoaderTest);
}
}
運行結果:
在代碼里定義了一個簡單的類加載器,使用這個類加載器去加載cn.fighter3.loader.ClassLoaderTest
類並創建實例,去做類型檢查的時候,發現結果是false。
2、雙親委派模型
從Java虛擬機的角度來看,只存在兩種不同的類加載器:一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現,是虛擬機自身的一部分;另外一種就是其他所有的類加載器,這些類加載器都由Java語言實現,獨立存在於虛擬機外部,並且全都繼承自抽象類 java.lang.ClassLoader。
站在Java開發人員的角度來看,類加載器就應當划分得更細致一些。自JDK 1.2以來,Java一直保持着三層類加載器、雙親委派的類加載架構。
雙親委派模型如上圖:
- 啟動類加載器(Bootstrap Class Loader):負責加載存放在 <JAVA_HOME>\lib目錄,或者被-Xbootclasspath參數所指定的路徑中存放的,能被Java虛擬機能夠識別的(按照文件名識別,如rt.jar、tools.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類。
- 擴展類加載器(Extension Class Loader):負責加載<JAVA_HOME>\lib\ext目錄中,或者被java.ext.dirs系統變量所指定的路徑中所有的類庫。
- 應用程序類加載器(Application Class Loader):負責加載用戶類路徑 (ClassPath)上所有的類庫,如果沒有自定義類加載器,一般情況下這個加載器就是程序中默認的類加載器。
用戶還可以加入自定義的類加載器器來進行擴展。
雙親委派模型的工作過程:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到最頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求時,子加載器才會嘗試自己去完成加載。

為什么要用雙親委派機制呢?
答案是為了保證應用程序的穩定有序。
例如類java.lang.Object,它存放在rt.jar之中,通過雙親委派機制,保證最終都是委派給處於模型最頂端的啟動類加載器進行加載,保證Object的一致。反之,都由各個類加載器自行去加載的話,如果用戶自己也編寫了一個名為java.lang.Object的類,並放在程序的 ClassPath中,那系統中就會出現多個不同的Object類。
雙親委派模型的代碼實現非常簡單,在java.lang.ClassLoader.java
中有一個 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) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父類加載器拋出ClassNotFoundException
// 說明父類加載器無法完成加載請求
}
if (c == null) {
// 在父類加載器無法加載時
// 再調用本身的findClass方法來進行類加載
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;
}
}
3、破壞雙親委派模型
雙親委派機制在歷史上主要有三次破壞:
第一次破壞
雙親委派模型的第一次“被破壞”其實發生在雙親委派模型出現之前——即JDK 1.2面世以前的“遠古”時代。
由於雙親委派模型在JDK 1.2之后才被引入,但是類加載器的概念和抽象類 java.lang.ClassLoader則在Java的第一個版本中就已經存在,為了向下兼容舊代碼,所以無法以技術手段避免loadClass()被子類覆蓋的可能性,只能在JDK 1.2之后的java.lang.ClassLoader中添加一個新的 protected方法findClass(),並引導用戶編寫的類加載邏輯時盡可能去重寫這個方法,而不是在 loadClass()中編寫代碼。
第二次破壞
雙親委派模型的第二次“被破壞”是由這個模型自身的缺陷導致的,如果有基礎類型又要調用回用戶的代碼,那該怎么辦呢?
例如我們比較熟悉的JDBC:
各個廠商各有不同的JDBC的實現,Java在核心包\lib
里定義了對應的SPI,那么這個就毫無疑問由啟動類加載器
加載器加載。
但是各個廠商的實現,是沒辦法放在核心包里的,只能放在classpath
里,只能被應用類加載器
加載。那么,問題來了,啟動類加載器它就加載不到廠商提供的SPI服務代碼。
為了解決這個我呢提,引入了一個不太優雅的設計:線程上下文類加載器 (Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContext-ClassLoader()方法進行設置,如果創建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局范圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器。
JNDI服務使用這個線程上下文類加載器去加載所需的SPI服務代碼,這是一種父類加載器去請求子類加載器完成類加載的行為。
第三次破壞
雙親委派模型的第三次“被破壞”是由於用戶對程序動態性的追求而導致的,例如代碼熱替換(Hot Swap)、模塊熱部署(Hot Deployment)等。
OSGi實現模塊化熱部署的關鍵是它自定義的類加載器機制的實現,每一個程序模塊(OSGi中稱為 Bundle)都有一個自己的類加載器,當需要更換一個Bundle時,就把Bundle連同類加載器一起換掉以實現代碼的熱替換。在OSGi環境下,類加載器不再雙親委派模型推薦的樹狀結構,而是進一步發展為更加復雜的網狀結構。
"簡單的事情重復做,重復的事情認真做,認真的事情有創造性地做!"——
我是三分惡,可以叫我老三/三分/三哥/三子,一個能文能武的全棧開發,咱們下期見!
參考:
【1】:《深入理解Java虛擬機:JVM高級特性與最佳實踐(第3版) 》