Tomcat 第六篇:類載入機制

極客挖掘機發表於2020-10-09

1. 引言

Tomcat 在部署 Web 應用的時候,是將應用放在 webapps 資料夾目錄下,而 webapps 對應到 Tomcat 中是容器 Host ,裡面的資料夾則是對應到 Context ,在 Tomcat 啟動以後, webapps 中的所有的 Web 應用都可以提供服務。

這裡會涉及到一個問題, webapps 下面不止會有一個應用,比如有 APP1 和 APP2 兩個應用,它們分別有自己獨立的依賴 jar 包,這些 jar 包會位於 APP 的 WEB-INFO/lib 這個目錄下,這些 jar 包大概率是會有重複的,比如常用的 Spring 全家桶,在這裡面,版本肯定會有不同,那麼 Tomcat 是如何處理的?

2. JVM 類載入機制

說到 Tomcat 的類載入機制,有一個繞不開的話題是 JVM 是如何進行類載入的,畢竟 Tomcat 也是執行在 JVM 上的。

以下內容參考自周志明老師的 「深入理解 Java 虛擬機器」。

2.1 什麼是類的載入

類的載入指的是將類的 .class 檔案中的二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,然後在堆區建立一個 java.lang.Class 物件,用來封裝類在方法區內的資料結構。類的載入的最終產品是位於堆區中的 Class 物件, Class 物件封裝了類在方法區內的資料結構,並且向 Java 程式設計師提供了訪問方法區內的資料結構的介面。

類載入器並不需要等到某個類被 「首次主動使用」 時再載入它, JVM 規範允許類載入器在預料某個類將要被使用時就預先載入它,如果在預先載入的過程中遇到了 .class 檔案缺失或存在錯誤,類載入器必須在程式首次主動使用該類時才報告錯誤( LinkageError 錯誤)如果這個類一直沒有被程式主動使用,那麼類載入器就不會報告錯誤。

載入.class檔案的方式
– 從本地系統中直接載入
– 通過網路下載.class檔案
– 從zip,jar等歸檔檔案中載入.class檔案
– 從專有資料庫中提取.class檔案
– 將Java原始檔動態編譯為.class檔案

2.2 類生命週期

接下來,我們看下一個類的生命週期:

一個型別從被載入到虛擬機器記憶體中開始,到解除安裝出記憶體為止,它的整個生命週期將會經歷載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和解除安裝(Unloading)七個階段,其中驗證、準備、解析三個部分統稱為連線(Linking)。

2.3 雙親委派模型

Java 提供三種型別的系統類載入器:

  • 啟動類載入器(Bootstrap ClassLoader):由 C++ 語言實現,屬於 JVM 的一部分,其作用是載入 <JAVA_HOME>\lib 目錄中的檔案,或者被 -Xbootclasspath 引數所指定的路徑中的檔案,並且該類載入器只載入特定名稱的檔案(如 rt.jar ),而不是該目錄下所有的檔案。啟動類載入器無法被 Java 程式直接引用。
  • 擴充套件類載入器( Extension ClassLoader ):由 sun.misc.Launcher.ExtClassLoader 實現,它負責載入 <JAVA_HOME>\lib\ext 目錄中的,或者被 java.ext.dirs 系統變數所指定的路徑中的所有類庫,開發者可以直接使用擴充套件類載入器。
  • 應用程式類載入器( Application ClassLoader ):也稱系統類載入器,由 sun.misc.Launcher.AppClassLoader 實現。負責載入使用者類路徑( Class Path )上所指定的類庫,開發者可以直接使用這個類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。

雙親委派模型的工作機制:

如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。

為什麼?

例如類 java.lang.Object ,它存放在 rt.jar 之中。無論哪一個類載入器都要載入這個類。最終都是雙親委派模型最頂端的 Bootstrap 類載入器去載入。因此 Object 類在程式的各種類載入器環境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類載入器自行去載入的話,如果使用者編寫了一個稱為 「java.lang.Object」 的類,並存放在程式的 ClassPath 中,那系統中將會出現多個不同的 Object 類, java 型別體系中最基礎的行為也就無法保證,應用程式也將會一片混亂。

3. Tomcat 類載入機制

先整體看下 Tomcat 類載入器:

可以看到,在原來的 JVM 的類載入機制上面, Tomcat 新增了幾個類載入器,包括 3 個基礎類載入器和每個 Web 應用的類載入器。

3 個基礎類載入器在 conf/catalina.properties 中進行配置:

common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"

server.loader=

shared.loader=
  • Common: 以應用類載入器為父類,是 Tomcat 頂層的公用類載入器,其路徑由 conf/catalina.properties 中的 common.loader 指定,預設指向 ${catalina.home}/lib 下的包。
  • Catalina: 以 Common 類載入器為父類,是用於載入 Tomcat 應用伺服器的類載入器,其路徑由 server.loader 指定,預設為空,此時 Tomcat 使用 Common 類載入器載入應用伺服器。
  • Shared: 以 Common 類載入器為父類,是所有 Web 應用的父類載入器,其路徑由 shared.loader 指定,預設為空,此時 Tomcat 使用 Common 類載入器作為 Web 應用的父載入器。
  • Web 應用: 以 Shared 類載入器為父類,載入 /WEB-INF/classes 目錄下的未壓縮的 Class 和資原始檔以及 /WEB-INF/lib 目錄下的 jar 包,該類載入器只對當前 Web 應用可見,對其他 Web 應用均不可見。

4. Tomcat 類載入機制原始碼

4.1 ClassLoader 的建立

先看下載入器類圖:

先從 BootStrap 的 main 方法看起:

public static void main(String args[]) {
    synchronized (daemonLock) {
        if (daemon == null) {
            // Don't set daemon until init() has completed
            Bootstrap bootstrap = new Bootstrap();
            try {
                bootstrap.init();
            } catch (Throwable t) {
                handleThrowable(t);
                t.printStackTrace();
                return;
            }
            daemon = bootstrap;
        } else {
            // When running as a service the call to stop will be on a new
            // thread so make sure the correct class loader is used to
            // prevent a range of class not found exceptions.
            Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
        }
        // 省略其餘程式碼...
    }
}

可以看到這裡先判斷了 bootstrap 是否為 null ,如果不為 null 直接把 Catalina ClassLoader 設定到了當前執行緒,如果為 null 下面是走到了 init() 方法。

public void init() throws Exception {
    // 初始化類載入器
    initClassLoaders();
    // 設定執行緒類載入器,將容器的載入器傳入
    Thread.currentThread().setContextClassLoader(catalinaLoader);
    // 設定區安全類載入器
    SecurityClassLoad.securityClassLoad(catalinaLoader);
    // 省略其餘程式碼...
}

接著這裡看到了會呼叫 initClassLoaders() 方法進行類載入器的初始化,初始化完成後,同樣會設定 Catalina ClassLoader 到當前執行緒。

private void initClassLoaders() {
    try {
        commonLoader = createClassLoader("common", null);
        if (commonLoader == null) {
            // no config file, default to this loader - we might be in a 'single' env.
            commonLoader = this.getClass().getClassLoader();
        }
        catalinaLoader = createClassLoader("server", commonLoader);
        sharedLoader = createClassLoader("shared", commonLoader);
    } catch (Throwable t) {
        handleThrowable(t);
        log.error("Class loader creation threw exception", t);
        System.exit(1);
    }
}

看到這裡應該就清楚了,會建立三個 ClassLoader : CommClassLoader , Catalina ClassLoader , SharedClassLoader ,正好對應前面介紹的三個基礎類載入器。

接著進入 createClassLoader() 檢視程式碼:

private ClassLoader createClassLoader(String name, ClassLoader parent)
    throws Exception {

    String value = CatalinaProperties.getProperty(name + ".loader");
    if ((value == null) || (value.equals("")))
        return parent;

    value = replace(value);

    List<Repository> repositories = new ArrayList<>();

    String[] repositoryPaths = getPaths(value);

    for (String repository : repositoryPaths) {
        // Check for a JAR URL repository
        try {
            @SuppressWarnings("unused")
            URL url = new URL(repository);
            repositories.add(new Repository(repository, RepositoryType.URL));
            continue;
        } catch (MalformedURLException e) {
            // Ignore
        }

        // Local repository
        if (repository.endsWith("*.jar")) {
            repository = repository.substring
                (0, repository.length() - "*.jar".length());
            repositories.add(new Repository(repository, RepositoryType.GLOB));
        } else if (repository.endsWith(".jar")) {
            repositories.add(new Repository(repository, RepositoryType.JAR));
        } else {
            repositories.add(new Repository(repository, RepositoryType.DIR));
        }
    }

    return ClassLoaderFactory.createClassLoader(repositories, parent);
}

可以看到,這裡載入的資源正好是我們剛才看到的配置檔案 conf/catalina.properties 中的 common.loaderserver.loadershared.loader

4.2 ClassLoader 載入過程

直接開啟 ParallelWebappClassLoader ,至於為啥不是看 WebappClassLoader ,從名字上就知道 ParallelWebappClassLoader 是一個並行的 WebappClassLoader 。

然後看下 ParallelWebappClassLoader 的 loadclass 方法是在它的父類 WebappClassLoaderBase 中實現的。

4.2.1 第一步:

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

    synchronized (getClassLoadingLock(name)) {
        if (log.isDebugEnabled())
            log.debug("loadClass(" + name + ", " + resolve + ")");
        Class<?> clazz = null;

        // Log access to stopped class loader
        checkStateForClassLoading(name);

        // (0) Check our previously loaded local class cache
        clazz = findLoadedClass0(name); 
        if (clazz != null) {
            if (log.isDebugEnabled())
                log.debug("  Returning class from cache");
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }
        // 省略其餘...

首先呼叫 findLoaderClass0() 方法檢查 WebappClassLoader 中是否載入過此類。

protected Class<?> findLoadedClass0(String name) {

    String path = binaryNameToPath(name, true);

    ResourceEntry entry = resourceEntries.get(path);
    if (entry != null) {
        return entry.loadedClass;
    }
    return null;
}

WebappClassLoader 載入過的類都存放在 resourceEntries 快取中。

protected final Map<String, ResourceEntry> resourceEntries = new ConcurrentHashMap<>();

4.2.2 第二步:

    // 省略其餘...
    clazz = findLoadedClass(name);
    if (clazz != null) {
        if (log.isDebugEnabled())
            log.debug("  Returning class from cache");
        if (resolve)
            resolveClass(clazz);
        return clazz;
    }
    // 省略其餘...

如果第一步沒有找到,則繼續檢查 JVM 虛擬機器中是否載入過該類。呼叫 ClassLoader 的 findLoadedClass() 方法檢查。

4.2.3 第三步:

    ClassLoader javaseLoader = getJavaseClassLoader();
    boolean tryLoadingFromJavaseLoader;
    try {

        URL url;
        if (securityManager != null) {
            PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
            url = AccessController.doPrivileged(dp);
        } else {
            url = javaseLoader.getResource(resourceName);
        }
        tryLoadingFromJavaseLoader = (url != null);
    } catch (Throwable t) {

        ExceptionUtils.handleThrowable(t);

        tryLoadingFromJavaseLoader = true;
    }

    if (tryLoadingFromJavaseLoader) {
        try {
            clazz = javaseLoader.loadClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
    }

如果前兩步都沒有找到,則使用系統類載入該類(也就是當前 JVM 的 ClassPath )。為了防止覆蓋基礎類實現,這裡會判斷 class 是不是 JVMSE 中的基礎類庫中類。

4.2.4 第四步:

    boolean delegateLoad = delegate || filter(name, true);

    // (1) Delegate to our parent if requested
    if (delegateLoad) {
        if (log.isDebugEnabled())
            log.debug("  Delegating to parent classloader1 " + parent);
        try {
            clazz = Class.forName(name, false, parent);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Loading class from parent");
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
    }

先判斷是否設定了 delegate 屬性,設定為 true ,那麼就會完全按照 JVM 的"雙親委託"機制流程載入類。

若是預設的話,是先使用 WebappClassLoader 自己處理載入類的。當然,若是委託了,使用雙親委託亦沒有載入到 class 例項,那還是最後使用 WebappClassLoader 載入。

4.2.5 第五步:

    if (log.isDebugEnabled())
        log.debug("  Searching local repositories");
    try {
        clazz = findClass(name);
        if (clazz != null) {
            if (log.isDebugEnabled())
                log.debug("  Loading class from local repository");
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }
    } catch (ClassNotFoundException e) {
        // Ignore
    }

若是沒有委託,則預設會首次使用 WebappClassLoader 來載入類。通過自定義 findClass() 定義處理類載入規則。

findClass() 會去 Web-INF/classes 目錄下查詢類。

4.2.6 第六步:

    if (!delegateLoad) {
        if (log.isDebugEnabled())
            log.debug("  Delegating to parent classloader at end: " + parent);
        try {
            clazz = Class.forName(name, false, parent);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Loading class from parent");
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
    }

若是 WebappClassLoader 在 /WEB-INF/classes/WEB-INF/lib 下還是查詢不到 class ,那麼無條件強制委託給 System 、 Common 類載入器去查詢該類。

4.2.7 小結

Web 應用類載入器預設的載入順序是:

  1. 先從快取中載入;
  2. 如果沒有,則從 JVM 的 Bootstrap 類載入器載入;
  3. 如果沒有,則從當前類載入器載入(按照 WEB-INF/classes 、 WEB-INF/lib 的順序);
  4. 如果沒有,則從父類載入器載入,由於父類載入器採用預設的委派模式,所以載入順序是 AppClassLoader 、 Common 、 Shared 。

參考

https://www.jianshu.com/p/69c4526b843d

相關文章