Tomcat 第六篇:類加載機制

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 。

參考

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

Tags: