類加載器與雙親委派模型

類加載器

虛擬機設計團隊把類加載階段中的 “通過一個類的全限定名來獲取描述此類的二進制字節流” 這個動作放到 Java 虛擬機外部去實現,以便讓應用程序自己決定如何去獲取所需要的類。實現這個動作的代碼模塊稱爲 “類加載器”。

類與類加載器

類加載器雖然只用於實現類的加載動作,但它在 Java 程序中起到的作用卻遠遠不限於類加載階段。對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在 Java 虛擬機中的唯一性,每一個類加載器都有一個獨立的類命名空間。也就是說:比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源於同一個 Class 文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。
這裏所指的“相等”,包括代表類的 Class 對象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法返回的結果,也包括使用 instanceof 關鍵字做對象所屬關係判定等情況。

雙親委派模型

從 Java 虛擬機的角度來講,只存在兩種不同的類加載器:一種是啓動類加載器(Bootstrap ClassLoader),這個類加載器使用 C++ 語言實現,是虛擬機自身的一部分;另一種就是所有其他的類加載器,這些類加載器都由 Java 語言實現,獨立於虛擬機外部,並且全都繼承自抽象類 java.lang.ClassLoader 。
從開發的角度看,類加載器還可以進一步劃分,絕大部分 Java 程序都會使用到以下 3 種系統提供的類加載器。

  • 啓動類加載器(Bootstrap ClassLoader): 這個類加載器負責將存放在 <JAVA_HOME>\lib 目錄中的,並且是虛擬機識別的(僅按照文件名識別)類庫加載到虛擬機內存中。啓動類加載器無法被 Java 程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給引導類加載器,那直接使用 null 代替即可。例如 java.lang.ClassLoader.getClassLoader() 方法:
@CallerSensitive
public ClassLoader getClassLoader() {
        ClassLoader cl = getClassLoader0();
        if (cl == null)
            return null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
        }
        return cl;
    }
  • 擴展類加載器(Extension ClassLoader): 這個加載器由 sun.misc.Launcher$ExtClassLoader 實現,它負責加載 <JAVA_HOME>\lib\ext 目錄中的,或者被 java.ext.dirs 系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器。
  • 應用程序類加載器(Application ClassLoader): 這個類加載器由 sun.misc.Launcher$AppLcassLoader 實現。由於這個類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以一般也稱它爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

這些類加載器之間的層次關係如下:
在這裏插入圖片描述
上圖中展示的類加載器之間的這種層次關係,稱爲類加載器的雙親委派模型(Parents Delegation Model)。雙親委派模型要求除了頂層的啓動類加載器外,其餘的類加載器都應當有自己的父類加載器。這裏類加載器之間的父子關係一般不會用繼承的關係實現,而是用組合關係來複用父加載器的代碼。
雙親委派模型是 Java 設計者推薦給開發者的一種類加載器實現方式,其工作過程是:如果一個類加載器收到了類加載的請求,它首先不會自己起嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳遞到頂層的啓動類加載器中,只有當父加載器反饋自己無法完成這個加載請求時,子加載器纔會嘗試自己去加載。
使用雙親委派模型來組織類加載器之間的關係,對於保證 Java 程序的穩定運作很重要,還有一個顯而易見的好處就是 Java 類隨着它的類加載器一起具備了一種帶有優先級的層次關係。比如 Object 類存放在 rt.jar 中,無論哪個類加載器要加載這個類,都會委派給最頂端的啓動類加載器完成,如果用戶自己編寫了一個 java.lang.Object 的類,則不會被加載運行。

破壞雙親委派模型

雙親委派模型並不是一個強制性的約束,所以出現過“被破壞”的情況。
第一次是發生在雙親委派模型出現之前,由於雙親委派模型在 JDK 1.2 之後才被引入,而類加載器和抽象類 java.lang.ClassLoader 則在 JDK 1.0 時代就已經存在,爲了向前兼容,java.lang.ClassLoader 增加了一個新的 protected 方法 findClass()。
第二次是由於雙親委派模型自身的缺陷導致的,雙親委派很好地解決了各個類加載器的基礎類的統一問題,但是如果基礎類又要調回用戶代碼該怎麼辦?
一個典型的例子就是 JNDI 服務,它的代碼由啓動類加載器加載,但 JNDI 的目的就是對資源進行集中管理和查找,它需要調用由獨立廠商實現並部署在應用程序的 ClassPath 下的 JNDI 接口提供者(SPI,Service Provider Interface)的代碼,但啓動類不可能“認識”這些代碼,這時怎麼辦?
爲了解決這個問題,Java 設計團隊引入了線程上下文類加載器(Thread Context ClassLoader)。這個類加載器可以通過 Thread 類的 setContextClassLoader() 方法進行設置,如果創建線程時還未設置,它將從父線程中繼承一個,如果在應用程序的全局範圍內都沒有設置過,那這個類加載器默認就是應用程序類加載器。
有了線程上下文類加載器,JNDI 服務使用這個線程上下文類加載器去加載所需要的 SPI 代碼,也就是父類加載器請求子類加載器去完成類加載的動作,這種行爲實際上就是打通了雙親委派模型的層次結構來逆向使用類加載器。Java 中所有涉及 SPI 的加載動作基本上都採用這種方式,例如 JNDI、JDBC 等。
第三次是由於用戶對程序動態性的追求而導致的,比如:代碼熱替換、模塊熱部署等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章