天天看點

類加載器與雙親委派模型類加載器

文章目錄

  • 類加載器
    • 類與類加載器
    • 雙親委派模型
    • 破壞雙親委派模型

類加載器

虛拟機設計團隊把類加載階段中的 “通過一個類的全限定名來擷取描述此類的二進制位元組流” 這個動作放到 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 等。

第三次是由于使用者對程式動态性的追求而導緻的,比如:代碼熱替換、子產品熱部署等。

繼續閱讀