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

類加載器簡介

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

對於任意一個類,被同一個類加載器加載後都是唯一的,但如果被不同加載器加載後,就不是唯一的了。即使是源於同一個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裏,但是被不同的類加載器加載後,仍然是兩個獨立的類。

類加載器的劃分

除了像上面例子代碼中,我們自己實現的自定義類加載器,還有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來實現,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

雙親委託模型

之前提到,對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在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設計者們推薦給開發者們的類加載器實現方式。大部分的類加載器都遵循這個模型,但也有例外的情況,比如下面這三種情況:

重寫ClassLoader的loadClass()方法

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

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()方法進行設置。

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

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

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

總結

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

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