【JVM】類載入器與雙親委派

數小錢錢的種花兔發表於2020-12-09

類載入器,顧名思義,即是實現類載入的功能模組,負責將Class的位元組碼形式載入成記憶體形式的Class物件。位元組碼檔案可來源於磁碟或者jar包中的Class檔案,也可以來自網路位元組流。

類載入器

在JVM中,內建了三個重要的類載入器,Application classLoader,Extension classLoader和Bootstrap classLoader。應用類載入器,擴充套件類載入器和啟動類載入器。

  • Bootstrap classLoader啟動類載入器:載入JAVA_HOME/lib/rt.jar下的核心類,比如 java.util.*、java.io.*、java.nio.*、java.lang.* 等等。使用C程式碼實現,Java無法訪問。
  • Extension classLoader擴充套件類載入器:載入JAVA_HOME/lib/ext/*.jar中的擴充套件類,比如 swing 系列、內建的 js 引擎、xml 解析器 等等,這些庫名通常以 javax 開頭。
  • Application classLoader應用類載入器:載入Classpath環境變數裡定義的路徑中的jar包和目錄。自己編寫的程式碼和第三方jar都由該類載入器載入。

三種類載入器存在傳遞性。Application classLoader 載入類時,會先問問Extension classLoader是否載入過,會在再問問Bootstrap classLoader是否載入過。

每個Class物件裡面都有一個classLoader屬性記錄當前類由哪個類載入器載入

雙親委派模型

雙親委派機制也很好理解,AppClassLoader只負責載入ClassPath下的class檔案,需要載入系統類庫時,會委託上級類載入器,BootstrapClassLoader和ExtensionClassLoader,去載入對應的類庫,這就是所謂的“雙親委派模型”

下面通過原始碼來分析雙親委派的流程。此處的loadClass方法來源於類載入器抽象類ClassLoader。該方法是載入類的入口。使用者可以繼承ClassLoader來自定義類載入器。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        Class<?> c = findLoadedClass(name);//檢查名稱為name的類是否被本類載入器載入過
        if (c == null) {// 為null表示沒有被載入
            long t0 = System.nanoTime();
            try {
                if (parent != null) {//上級不為null
                    c = parent.loadClass(name, false);//遞迴呼叫上級類載入器loadClass方法
                } else {//上級為null,即表示上級是啟動類載入器
                    c = findBootstrapClassOrNull(name);//委託啟動類載入器在javaHome/jre/lib下尋找名稱為name的類
                }
            } catch (ClassNotFoundException e) {
                // 如果上級類載入器沒有找到名稱為name的類,則在此處捕獲ClassNotFoundException異常
            }

            if (c == null) {//c為null表示上級類載入器沒有找到name類
                long t1 = System.nanoTime();
                c = findClass(name);//到本載入器的路徑下尋找名稱為name的類(例如擴充套件類載入器則是lib/ext/下)

                // 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;
    }
}

綜上,類載入器載入類的具體流程如下:

  1. 先檢查本類載入器是否載入類,如果已載入,結束,沒載入進入下一步;
  2. 遞迴委派上級類載入器;
  3. 如果沒有上級了,執行啟動類載入器,查詢類是否存在對應的路徑下;
  4. 上級都沒有找到類時,跳出遞迴,在本類載入器對應路徑下查詢類。

雙親委派的好處:根據雙親委派模型的特點,可以知道,越是基礎的類,由越上層的類載入器來載入,如此一來,Java類隨著類載入具備了一個帶有優先順序地層次關係。這樣可以保證,若使用者編寫了與Java類庫中的類重名的類,此類不會被載入,因為同名的類往往是會被委派給啟動類載入器或擴充套件類載入器來載入。因此雙親委派機制可以保證Java程式的穩定性

破壞雙親委派

在雙親委派機制中,我們知道,基礎的類由上級類載入器載入。雙親委派可以保證“基礎類作為API被使用者程式碼呼叫”這個場景能夠準確執行。但是,可能會存在一些基礎類呼叫使用者程式碼的情況。例如,Java提供了很多服務提供者介面(Service Provider Interface, SPI),允許第三方為這些介面提供實現,常見的SPI實現有JDBC、JCE、JNDI、JAXP等。SPI的介面由Java核心庫提供,而其實現程式碼則是屬於應用程式的jar包(放進CLASSPATH中)。那麼問題來了,核心庫中的SPI的介面由啟動類載入來載入,CLASSPATH中的實現類由應用類載入器來載入,此種應用場景下,啟動類載入器是無法找到SPI的實現類的。

因此,需要通過某種特殊手段,來打破雙親委派,讓上級類載入器找不到類時,呼叫能獲取到目標類的下級類載入器來進行載入。jdk引入了“執行緒上下文類載入器”來解決此問題。

執行緒上下文類載入器

在Thread類中有一個成員變數,contextClassLoader,如下所示

class Thread {
  ...
  private ClassLoader contextClassLoader;

  public ClassLoader getContextClassLoader() {
    return contextClassLoader;
  }

  public void setContextClassLoader(ClassLoader cl) {
    this.contextClassLoader = cl;
  }
  ...
}

這個contextClassLoader就是執行緒上下文類載入器,用於引用一個類載入器。可以通過setContextClassLoader方法進行設定,若不設定,執行緒會從父執行緒中繼承一個類載入器。main執行緒的contextClassLoader是應用類載入器,因此預設情況contextClassLoader都指向AppClassLoader

按照雙親委派的機制,上級的BootStrapClassLoader無法委派下級AppClassLoader來載入類,但是可以通過執行緒中的contextClassLoader來獲取到AppClassLoader進行類載入。如此一來,便可以打破雙親委派的層次結構來逆向使用類載入器。

相關文章