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

前言

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

ClassLoader 介紹

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

class loading

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

類加載器的一個做用就是將編譯器編譯生成的二進制 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 默認的類加載器,咱們還能夠自定義類加載器,後文會分析如何自定義類加載器。spring

雙親委派模型是什麼

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

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

classLoader

三個原則的具體體現是:數組

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

  • 「可見性原則」 體如今容許子類加載器查看父類加載器加載的全部類,可是父類加載器不能查看子類加載器加載的類。tomcat

  • 「惟一性原則」 體如今雙親委派整個機制保證了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

參考資料 & 鳴謝

相關文章
相關標籤/搜索