1. 程式人生 > >月薪過萬必會的:雙親委託模型

月薪過萬必會的:雙親委託模型

類載入器簡介

在介紹雙親委託模型之前,先介紹一下類載入器。類載入器通過一個類的全限定名來轉換為描述這個類的二進位制位元組流。

對於任意一個類,被同一個類載入器載入後都是唯一的,但如果被不同載入器載入後,就不是唯一的了。即使是源於同一個Class檔案、被同一個JVM載入,只要載入類的載入器不同,那麼類就不同。

如何判斷類是否相同,可以使用Class物件的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果進行判斷,也可以使用instanceof關鍵字進行物件所屬關係的判斷。
下面我們寫一個不同類載入器載入後的類,看一下對instanceof關鍵字運算有什麼影響:

public class OneMoreStudy {
    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 inputStream = getClass().getResourceAsStream(fileName);
                    if (inputStream == null) {
                        return super.loadClass(name);
                    }
                    byte[] array = new byte[inputStream.available()];
                    inputStream.read(array);
                    return defineClass(name, array, 0, array.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };
        Object object = myLoader.loadClass("OneMoreStudy").newInstance();

        System.out.println("class name: " + object.getClass().getName());
        System.out.println("instanceof: " + (object instanceof OneMoreStudy));
    }
}

執行結果:

class name: OneMoreStudy
instanceof: false

在執行結果中,第一行可以看出這個物件確實是OneMoreStudy類例項化出來的,但在第二行中instanceof運算結果是false,說明在JVM中存在兩個OneMoreStudy類,一個是由系統應用程式類載入器載入的,另一個是由我們自定義的類載入器載入的。雖然都是來自同一個Class檔案,在同一個JVM裡,但是被不同的類載入器載入後,仍然是兩個獨立的類。

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。

類載入器的劃分

除了像上面例子程式碼中,我們自己實現的自定義類載入器,還有3種系統提供的類載入器:

  1. 啟動類載入器(Bootstrap ClassLoader):它負責將存放在%JAVA_HOME%\lib目錄中的,或者被-Xbootclasspath引數所指定的路徑中的,並且是JVM識別的類庫載入到JVM記憶體中。它僅按照檔名識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被載入。它是由C++語言實現的,無法被Java程式直接引用。

  2. 擴充套件類載入器(Extension ClassLoader):它負責載入%JAVA_HOME%\lib\ext目錄中的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫。它由sun.misc.Launcher.ExtClassLoader實現,開發者可以直接使用擴充套件類載入器。

  3. 應用程式類載入器(Application ClassLoader):它負責載入使用者類路徑(ClassPath)上所指定的類庫。由於它是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統類載入器。它由sun.misc.Launcher.AppClassLoader來實現,開發者可以直接使用這個類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。

雙親委託模型

之前提到,對於任意一個類,都需要由載入它的類載入器和這個類本身一同確立其在JVM中的唯一性。可是有這麼多種的類載入器,如何保證一個類在JVM中的唯一性呢?為了解決這個問題,雙親委託模型(Parents Delegation Model)應運而生,它就是下圖所展示的類載入器之間的層次關係:

除了頂層的啟動類載入器外,其餘的類載入器都必須有自己的父類載入器。類載入器之間的父子關係,一般不會以繼承的關係來實現,而是都使用組合關係來複用父類載入器。

類載入器收到類載入的請求後,它不會首先自己去嘗試載入這個類,而是把這個請求委派給父類載入器去嘗試載入。每一個類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中。只有當父類載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。這樣就保證了類在JVM中的唯一性,也保證了Java程式穩定運作。

實現雙親委派模型的程式碼都集中在java.lang.ClassLoader的loadClass()方法之中,如下:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        //首先,檢查該類是否已經被載入過了
        Class<?> c = findLoadedClass(name);
        //如果沒有載入過,就呼叫父類載入器的loadClass()方法
        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);
                
                //記錄統計資料
                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設計者們推薦給開發者們的類載入器實現方式。大部分的類載入器都遵循這個模型,但也有例外的情況,比如下面這三種情況:

重寫ClassLoader的loadClass()方法

在上面例子程式碼中,就是重寫了ClassLoader的loadClass()方法,破壞了雙親委派模型,產生了不唯一的類。所以,不提倡開發人員覆蓋loadClass()方法,而應當把自己的類載入邏輯寫到findClass()方法中,在loadClass()方法的邏輯裡如果父類載入失敗,則會呼叫自己的findClass()方法來完成載入,這樣就可以保證新寫出來的類載入器是符合雙親委派模型。

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。

SPI(服務提供者介面)

Java提供了很多SPI(Service Provider Interface,服務提供者介面),允許第三方為這些介面提供實現,常見的SPI有JDBC、JNDI、JCE、JAXB和JBI等。

SPI的介面由Java核心庫來提供,而這些SPI的實現程式碼則是作為Java應用所依賴的jar包被包含進類路徑(ClassPath)裡。SPI介面中的程式碼經常需要載入具體的實現類。那麼問題來了,SPI的介面是Java核心庫的一部分,是由啟動類載入器來載入的;SPI的實現類是由系統類載入器來載入的。引導類載入器是無法找到SPI的實現類的,因為依照雙親委派模型,啟動類載入器無法委派系統類載入器來載入類。

這時候就會使用執行緒上下文類載入器(Thread Context ClassLoader),在JVM中會把當前執行緒的類載入器載入不到的類交給執行緒上下文類載入器來載入,直接使用Thread.currentThread().getContextClassLoader()來獲得,預設返回的就是應用程式類載入器,也可以通過java.lang.Thread類的setContextClassLoader()方法進行設定。

而執行緒上下文類載入器破壞了雙親委派模型,也就是父類載入器請求子類載入器去完成類載入的動作,但為了實現功能,這也是一種巧妙的實現方式。

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。

OSGi(開放服務閘道器協議)

OSGi(Open Service Gateway Initiative,開放服務閘道器協議)技術是面向Java動態化模組化系統模型,程式模組(稱為Bundle)無需重新引導可以被遠端安裝、啟動、升級和解除安裝。實現程式模組熱部署的關鍵則是它自定義的類載入器機制的實現。

在OSGi中,類載入器不再是雙親委派模型中的樹狀結構,而是一個較為複雜的網狀結構,類載入的規則簡要介紹如下:

  1. 若類屬於java.*包,則將載入請求委託給父載入器
  2. 若類定義在啟動委託列表(org.osgi.framework.bootdelegation)中,則將載入請求委託給父載入器
  3. 若類屬於在Import-Package中定義的包,則框架通過ClassLoader依賴關係圖找到匯出此包的Bundle的ClassLoader,並將載入請求委託給此ClassLoader
  4. 若類資源屬於在Require-Bundle中定義的Bundle,則框架通過ClassLoader依賴關係圖找到此Bundle的ClassLoader,將載入請求委託給此ClassLoader
  5. Bundle搜尋自己的類資源( 包括Bundle-Classpath裡面定義的類路徑和屬於Bundle的Fragment的類資源)
  6. 若類在DynamicImport-Package中定義,則開始嘗試在執行環境中尋找符合條件的Bundle

如果在經過上面一系列步驟後,仍然沒有正確地載入到類資源,則會向外丟擲類未發現異常。

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。

總結

類載入器通過一個類的全限定名來轉換為描述這個類的二進位制位元組流,可劃分為啟動類載入器、擴充套件類載入器、應用程式類載入器、自定義類載入器。在雙親委託模型中,將上述各種類載入器組成一系列的父子關係,子類載入器先把類載入請求委派給父類載入器去嘗試載入,父類載入器無法載入時子類載入器才自己嘗試載入,這樣保證了類在JVM中的唯一性。不過,也不遵循雙親委託模型的情況,比如:重寫ClassLoader的loadClass()方法、SPI(服務提供者介面)、OSGi(開放服務閘道器協議)。

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨