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

前言

我們經常會在面試中遇到有關類加載器的問題,而作爲一名Java開發人員應該瞭解類加載器如何工作?雙親委派模型是什麼?如何打破雙親委派?爲什麼打破?等等。所以今天的主題就是聊一聊類加載器。

ClassLoader 介紹

《深入理解Java虛擬機》這本書大家都不陌生,想必我們大多數人瞭解JVM知識都是通過這本書,在該書中也詳細介紹了Java類加載的全過程,包含加載、驗證、準備、解析和初始化這5個階段。

class loading

在加載階段,通過一個類的全限定名來獲取此類的二進制字節流,就是依靠類加載器來完成。

類加載器的一個作用就是將編譯器編譯生成的二進制 Class 文件加載到內存中,進而轉換成虛擬機中的類。Java系統提供了三種內置的類加載器:

  • 啓動類加載器 (Bootstrap Class Loader): 負責加載JDK核心類,通常是 rt.jar 和位於 $JAVA_HOME/jre/lib 下的核心庫.
  • 擴展類加載器 (Extensions Class Loader): 負責加載\jre\lib\ext目錄下 JAR 包
  • 系統類加載器 (System Class Loader):負責加載所有應用程序級別的類到JVM,它會加載classpath環境變量或 -classpath以及-cp命令行參數中指定的文件

當然,上面是 Java 默認的類加載器,我們還可以自定義類加載器,後文會分析如何自定義類加載器。

雙親委派模型是什麼

網上有文章分析說,類加載器遵循三個原則:委託性可見性唯一性原則。這三點其實都和雙親委派模型有關,雙親委派的工作過程如下:

當類加載器收到類的加載請求時,首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,所有的加載請求會傳送到頂層的啓動類加載器,只有父類加載器無法完成加載請求,纔會交由子加載器去加載。

classLoader

三個原則的具體體現是:

  • 「委託性原則」 體現在當子類加載器收到類的加載請求時,會將加載請求向上委託給父類加載器。

  • 「可見性原則」 體現在允許子類加載器查看父類加載器加載的所有類,但是父類加載器不能查看子類加載器加載的類。

  • 「唯一性原則」 體現在雙親委派整個機制保證了Java類的唯一性,假如你寫了一個和JRE核心類同名的類,比如Object類,雙親委派機制可以避免自定義類覆蓋核心類的行爲,因爲它首先會將加載類的請求,委託給ExtClassLoader去加載,ExtClassLoader再委託給BootstrapClassLoader,啓動類加載器如果發現已經加載了 Object類,那麼就不會加載自定義的Object類。

ClassLoader 如何工作

聊完雙親委派模型,你肯定想知道它是如何實現,那麼來看一下 ClassLoader 的核心方法,其中的 loadClass 方法就是實現雙親委派機制的關鍵,爲了縮短代碼篇幅和方便閱讀,去掉了一些代碼細節:

package java.lang;
public abstract class ClassLoader {

    protected Class defineClass(byte[] b); 
  
    protected Class<?> findClass(String name); 

    protected Class<?> loadClass(String name, boolean resolve) {
        synchronized (getClassLoadingLock(name)) {
            // 1. 檢查類是否已經被加載過
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        //2. 委託給父類加載
                        c = parent.loadClass(name, false);
                    } else {
                        //3. 父類不存在的,交給啓動類加載器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) { }
                if (c == null) {
                    //4. 父類加載器無法完成類加載請求時,調用自身的findClass方法來完成類加載
                    c = findClass(name);
                }
            }
            return c;
    }
}
  • defineClass 方法:調用 native 方法將 字節數組解析成一個 Class 對象。
  • findClass 方法:抽象類ClassLoader中默認拋出ClassNotFoundException,需要繼承類自己去實現,目的是通過文件系統或者網絡查找類
  • loadClass 方法: 首先根據類的全限定名檢查該類是否已經被加載過,如果沒有被加載,那麼當子加載器持有父加載器的引用時,那麼委託給父加載器去嘗試加載,如果父類加載器無法完成加載,再交給子類加載器進行加載。loadClass方法 就是實現了雙親委派機制。

現在我們熟悉了 ClassLoader 的三個重要方法,那麼如果需要自定義一個類加載器的話,直接繼承 ClassLoader類,一般情況只需要重寫 findClass 方法即可,自己定義加載類的路徑,可以從文件系統或者網絡環境。

但是,如果想打破雙親委派機制,那麼還要重寫 loadClass 方法,只不過,爲什麼我們要選擇去打破它呢? 我們常使用的 Tomcat的類加載器就打破了雙親委派機制,當然還有一些其他場景也打破了,比如涉及 SPI 的加載動作、熱部署等等。

接下來來看看 Tomcat 爲什麼打破雙親委派模型以及實現機制。

Tomcat如何打破雙親委派機制

爲什麼打破

現在都流行使用 springboot 開發 web 應用,Tomcat 內嵌在 springboot 中。而在此之前,我們會使用最原生的方式,servlet + Tomcat 的方式開發和部署 web 程序。web 應用的目錄結構大致如下:


| -  MyWebApp
      | -  WEB-INF/web.xml        -- 配置文件,用來配置Servlet等
      | -  WEB-INF/lib/           -- 存放Web應用所需各種JAR包
      | -  WEB-INF/classes/       -- 存放你的應用類,比如Servlet類
      | -  META-INF/              -- 目錄存放工程的一些信息

一個 Tomcat 可能會部署多個這樣的 web 應用,不同的 web 應用可能會依賴同一個第三方庫的不同版本,爲了保證每個 web 應用的類庫都是獨立的,需要實現類隔離。而Tomcat 的自定義類加載器 WebAppClassLoader 解決了這個問題,每一個 web 應用都會對應一個 WebAppClassLoader 實例,不同的類加載器實例加載的類是不同的,Web應用之間通各自的類加載器相互隔離。

當然 Tomcat自定義類加載器不只解決上面的問題,WebAppClassLoader 打破了雙親委派機制,即它首先自己嘗試去加載某個類,如果找不到再代理給父類加載器,其目的是優先加載Web應用定義的類

如何打破

WebappClassLoader 具體實現機制是重寫了 ClassLoader 的 findClass 和 loadClass 方法。

  • findClass 方法如下,省去部分細節:
public Class<?> findClass(String name) throws ClassNotFoundException {
    ...
    Class<?> clazz = null;
    try {
        //1. 先在 Web 應用目錄下查找類 
        clazz = findClassInternal(name);
    } catch (RuntimeException e) {
        throw e;
    }
    if (clazz == null) {
        try {
            //2. 如果在本地目錄沒有找到,交給父加載器去查找
            clazz = super.findClass(name);
        }  catch (RuntimeException e) {
           throw e;
        }
    }
    //3. 如果父類也沒找到,拋出 ClassNotFoundException
    if (clazz == null) {
        throw new ClassNotFoundException(name);
    }
    return clazz;
}
  • loadClass方法如下,省去部分細節:
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        Class<?> clazz = null;

        //1. 先在本地緩存查找該類是否已經加載過
        clazz = findLoadedClass0(name);
        if (clazz != null) {
            return clazz;
        }
        //2. 從系統類加載器的緩存中查找是否加載過
        clazz = findLoadedClass(name);
        if (clazz != null) {
            return clazz;
        }
        //3. 嘗試用 ExtClassLoader 類加載器類加載
        ClassLoader javaseLoader = getJavaseClassLoader();
        try {
            clazz = javaseLoader.loadClass(name);
            if (clazz != null) {
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
        // 4. 嘗試在本地目錄搜索 class 並加載
        try {
            clazz = findClass(name);
            if (clazz != null) {
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
        // 5. 嘗試用系統類加載器 (也就是 AppClassLoader) 來加載
        try {
            clazz = Class.forName(name, false, parent);
            if (clazz != null) {
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // 省略
        }
    }
    //6. 上述過程都加載失敗,拋出 ClassNotFoundException 異常
    throw new ClassNotFoundException(name);
}

從上面的代碼中可以看到,Tomcat 自定義的類加載器確實打破了雙親委派機制,同時根據 loadClass 方法的核心邏輯,我也畫了一張圖,描述了默認情況下 Tomcat 的類加載機制。

Tomcat loadClass

一開始將類加載請求委託給 ExtClassLoader,而不是委託給 AppClassLoader,這樣的原因是 防止 web 應用自己的類覆蓋JRE的核心類,如果 JRE 核心類中沒有該類,那麼才交給自定義的類加載器 WebappClassLoader 去加載。

小結

這篇文章主要總結了類加載器的雙親委派模型、雙親委派的工作機制、以及Tomcat如何打破雙親委派,當然有一些東西分享的比較簡單,比如 Tomcat 的類加載器這部分,沒有提及整個 Tomcat的類加載器層次結構,沒有提到 SharedClassLoader 和 CommonClassLoader 類加載器,這個等後續有時間再來分享。

同時,歡迎關注我新開的公衆號,定期分享Java後端知識!

pjmike

參考資料 & 鳴謝

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