在上一章裡,我們已經學習了類載入的過程,我們知道在載入階段需要”通過一個類的全限定名來獲取描述該類的二進位制位元組流“,而來完成這個工作的就是類載入器(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版) 》