1. 程式人生 > >Tomcat 原始碼分析 WebappClassLoader 分析 (基於8.0.5)

Tomcat 原始碼分析 WebappClassLoader 分析 (基於8.0.5)

0. 疑惑

在剛接觸 Tomcat 中的ClassLoader時心中不免冒出的疑惑: "Tomcat 裡面是怎麼樣設計ClassLoader的, 這樣設計有什麼好處?"; 我們先把這個問題留著, 到最後在看 !

1. Java 中 ClassLoader 類別

1. BootstrapClassLoader
    載入路徑: System.getProperty("java.class.path") 或直接通過 -Xbootclasspath 指定
    特性: 用C語言寫的
    手動獲取載入路徑: sun.misc.Launcher.getBootstrapClassPath().getURLs()

2. ExtClassLoader
    載入路徑: System.getProperty("java.ext.dirs") 或直接通過 -Djava.ext.dirs 指定
    特性: 繼承 URLClassLoader
    手動獲取載入路徑:((URLClassLoader)App.class.getClassLoader().getParent()).getURLs()


3. AppClassLoader
    載入路徑: System.getProperty("sun.boot.class.path") 或直接通過 -cp, -classpath 指定
    特性: 繼承 URLClassLoader
    手動獲取載入路徑: ((URLClassLoader)App.class.getClassLoader()).getURLs()
    通過 ClassLoader.getSystemClassLoader() 就可以獲取 AppClassLoader, 自己寫的程式中寫的 ClassLoader(繼承 URLClassLoader), 若不指定 parent, 預設的parent就是 AppClassLoader

PS:
AppClassLoader.getparent() = ExtClassLoader
ExtClassLoader.getParent() == null, 則直接通過 BootstrapClassLoader 來進行載入

2. Java 中 ClassLoader 主要方法

1. loadClass    方法 實現雙親委派模型
2. findClass    方法 根據Class名稱獲取Class路徑, 然後呼叫 defineClass 進行載入到JVM 記憶體中
3. defineClass  方法 加Class檔案的二進位制位元組碼載入到JVM記憶體生成Class物件
4. resolveClass 方法 JVM規範裡面指連線操作中的第三步操作, 實際上我們的平時使用的JDK並沒有按照JVM的這個規範進行設計, 你在進行debug時, 發現這個 resolveClass 永遠是 false

3. ClassLoader.loadClass() 方法

ClassLoader的雙親委派模式主要體現在 loadClass 方法上, 直接看程式碼

synchronized (getClassLoadingLock(name)) {              // 1. 通過一個ClassName對應一個 Object, 放到 ConcurrentHashMap 中, 最終通過 synchronized 實現併發載入
    Class<?> c = findLoadedClass(name);                 // 2. 檢視本 ClassLoader 是否載入過
    if (c == null) {
        try {
            if (parent != null) {                       // 4. parent != null, 則通過父ClassLoader來進行載入 (載入的原則是: class 一定要在 URLClassPath 中)
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);     // 5. parent == null, 則說明當前ClassLoader是ExtClassLoader, 直接通過 BootstrapClassLoader 來進行載入 (載入的原則是: class 一定要在 URLClassPath 中)
            }
        } catch (ClassNotFoundException e) {}
        if (c == null) {                                // 6. delegate 父 ClassLoader 還沒載入成功, 則用當前ClassLoader 來進行載入
            c = findClass(name);                        // 7. 通過 findClass 在本 ClassLoader 的path 上進行查詢 class, 轉化成 byte[], 通過 defineClass 載入到記憶體中 (載入的原則是: class 一定要在 URLClassPath 中)
        }
    }
    if (resolve) {                                      // 8. 永遠的 resolve = false, JVM規範指定是通過 resolveClass 方法實現 連結 操作的第三步, 實際我們的JVM上並沒有實現這個操作
        resolveClass(c);
    }
    return c;
}

4. ClassLoader 載入模式

下面通過一個簡單的Demo加深一下理解ClassLoader

Class A {
    public void doSomething(){
        B b = new B();
        b.doSomething();
    }

    public static void main(String[] args){
        A a = new A();
        a.doSomething()
    }
}

執行命令 java -classpath: test.jar A

操作步驟
1. AClass = AppClassLoader.loadClass(A)                                 # 通過 AppClassLoader 載入類A
2. BClass = AClass.getClassLoader().loadClass(B)                        # 其中通過 AClass.getClassLoader.getResource("/" + B.class.getName().replace(".", "/") + ".class") 查詢 B 的Resource
3. BClass.getDeclaredMethod("doSomething").invoke(BClass.newInstance()) # 直接啟用方法 doSomething

從中我們可以得知 在預設方法內進行 new 出物件, 其實是用的 Thread.currentThread().getContextClassloader() 來進行載入的 (A.class.getClassLoader() = B.class.getClassLoader())
有了上面的知識後我們再來看看 Tomcat 中的 ClassLoader

5. Tomcat 中 ClassLoader 的種類

1. BootstrapClassLoader : 系統類載入器
2. ExtClassLoader       : 擴充套件類載入器
3. AppClassLoader       : 普通類載入器
#下面是 這幾個 Classloader 是 Tomcat 對老版本的相容
4. commonLoader         : Tomcat 通用類載入器, 載入的資源可被 Tomcat 和 所有的 Web 應用程式共同獲取
5. catalinaLoader       : Tomcat 類載入器, 載入的資源只能被 Tomcat 獲取(但 所有 WebappClassLoader 不能獲取到 catalinaLoader 載入的類)
6. sharedLoader         : Tomcat 各個Context的父載入器, 這個類是所有 WebappClassLoader 的父類, sharedLoader 所載入的類將被所有的 WebappClassLoader 共享獲取
7. WebappClassLoader    : 每個Context 對應一個 WebappClassloader, 主要用於載入 WEB-INF/lib 與 WEB-INF/classes 下面的資源

這個版本 (Tomcat 8.x.x) 中, 預設情況下 commonLoader = catalinaLoader = sharedLoader
(PS: 為什麼這樣設計, 主要這樣這樣設計 ClassLoader 的層級後, WebAppClassLoader 就能直接訪問 tomcat 的公共資源, 若需要tomcat 有些資源不讓 WebappClassLoader 載入, 則直接在 ${catalina.base}/conf/catalina.properties 中的 server.loader 配置一下 載入路徑就可以了)

在看看下面的 UML 圖, 加深一下理解:

 

classLoader_ih.png

從新再來看一下 ClassLoader 的初始化

/**
 * 1. BootstrapClassLoader  : 系統類載入器
 * 2. ExtClassLoader        : 擴充套件類載入器
 * 3. AppClassLoader        : 普通類載入器
 #下面是 這幾個 Classloader 是 Tomcat 對老版本的相容
 * 4. commonLoader      : Tomcat 通用類載入器, 載入的資源可被 Tomcat 和 所有的 Web 應用程式共同獲取
 * 5. catalinaLoader    : Tomcat 類載入器, 載入的資源只能被 Tomcat 獲取(但 所有 WebappClassLoader 不能獲取到 catalinaLoader 載入的類)
 * 6. sharedLoader      : Tomcat 各個Context的父載入器, 這個類是所有 WebappClassLoader 的父類, sharedLoader 所載入的類將被所有的 WebappClassLoader 共享獲取
 *
 * 這個版本 (Tomcat 8.x.x) 中, 預設情況下 commonLoader = catalinaLoader = sharedLoader
 * (PS: 為什麼這樣設計, 主要這樣這樣設計 ClassLoader 的層級後, WebAppClassLoader 就能直接訪問 tomcat 的公共資源, 若需要tomcat 有些資源不讓 WebappClassLoader 載入, 則直接在 ${catalina.base}/conf/catalina.properties 中的 server.loader 配置一下 載入路徑就可以了)
 */
private void initClassLoaders() {
    ClassLoader classLoader = ClassLoader.getSystemClassLoader();
    try {                                                               // 1. 補充: createClassLoader 中程式碼最後呼叫 new URLClassLoader(array) 來生成 commonLoader, 此時 commonLoader.parent = null,  則採用的是預設的策略 Launcher.AppClassLoader
        commonLoader = createClassLoader("common", null);               // 2. 根據 catalina.properties 指定的 載入jar包的目錄, 生成對應的 URLClassLoader( 載入 Tomcat 中公共jar包的 classLoader, 這裡的 parent 引數是 null, 最終 commonLoader.parent 是 URLClassLoader)
        if( commonLoader == null ) {                                    // 3. 若 commonLoader = null, 則說明在 catalina.properties 裡面 common.loader 是空的
            // no config file, default to this loader - we might be in a 'single' env.
            commonLoader=this.getClass().getClassLoader();
        }
        catalinaLoader = createClassLoader("server", commonLoader);     // 4. 將 commonClassLoader 作為父 ClassLoader, 生成 catalinaLoader,這個類就是載入 Tomcat bootstrap.jar, tomcat-juli.jar 包的 classLoader (PS; 在 catalina.properties 裡面 server.loader 是空的, 則程式碼中將直接將 commonLoader 賦值給 catalinaLoader)
        sharedLoader = createClassLoader("shared", commonLoader);       // 5. 將 commonClassLoader 作為父 ClassLoader, 生成 sharedLoader, 這個類最後會作為所有 WebappClassLoader 的父類 ( PS: 因為 catalina.properties 裡面 shared.loader 是空的, 所以程式碼中直接將 commonLoader 賦值給 sharedLoader)
    } catch (Throwable t) {
        handleThrowable(t);
        log.error("Class loader creation threw exception", t);
        System.exit(1);
    }
}

額, 漏了, 漏了一個 JasperLoader, 這個classLoader 直接繼承 URLClassLoader, 當程式將 JSP 編譯成 servlet 的class之後, 通過這個 JasperLoader 進行載入(PS: 這個 JasperLoader 其實沒有什麼太多的功能);
接下來我們主要看 WebappClassLoader

6. WebappClassLoader 常見屬性

protected final Matcher packageTriggersDeny = Pattern.compile(                          // 在 delegating = false 的情況下, 被這個正則匹配到的 class 不會被 WebappClassLoader 進行載入 (其實就是 Tomcat 中的程式碼不能被 WebappClassLoader 來載入)
        "^javax\\.el\\.|" +
        "^javax\\.servlet\\.|" +
        "^org\\.apache\\.(catalina|coyote|el|jasper|juli|naming|tomcat)\\."
        ).matcher("");

protected final Matcher packageTriggersPermit =                                         // 在 delegating = false 的情況下, 下面正則匹配到的類會被 WebappClassLoader 進行載入
        Pattern.compile("^javax\\.servlet\\.jsp\\.jstl\\.").matcher("");

protected final ClassLoader parent;                                                     // WebappClassLoader 的父 parent(在這裡 Tomcat 8.x.x, parent  其實就是 commonClassloader)
protected final ClassLoader j2seClassLoader;                                            // 這個 classLoader 其實就是 ExtClassLoader (PS: 所有的 WebappClassLoader 出發到載入 J2SE 的類時, 直接通過 ExtClassLoader / BootstrapClassLoader 來進行載入 )
                                                                                
protected final Map<String, ResourceEntry> resourceEntries = new ConcurrentHashMap<>(); // 載入資源的時候會將 檔案快取在這個 Map 裡面, 下次就可以根據 ResourceEntry.lastModified 來判斷是否需要熱部署

protected WebResourceRoot resources = null;                                             // 這個 WebappClassLoader 載入的資源(PS: 其實就是 StandardRoot, 在WebappClassLoader 啟動時, 會載入 WEB-INF/lib 與 WEB-INF/classes 下的資源de URL加入 WebAppClassLoader的 URLClassPath 裡面)

private final HashMap<String,Long> jarModificationTimes = new HashMap<>();              // 儲存每個載入的資源, 上次修改的時間 (後臺定時任務檢查這個修改時間, 決定是否需要 reload)

7. WebappClassLoader 建構函式

parent: WebappClassLoader 的父classLoader, j2seClassLoader: ExtClassLoader, 所有WebappClassLoader 載入 J2SE 的類時, 需通過 ExtClassLoader 或 BootstartpClassLoader 來進行載入

public WebappClassLoader(ClassLoader parent) {              // 1. 在 Tomcat 8.x.x 中執行時, 會發現 parent 就是 commonClassLoader

    super(new URL[0], parent);

    ClassLoader p = getParent();                            // 2. 這裡做個檢查, 若建構函式傳來的 parent 是 null, 則 將 AppClassLoader 賦值給 WebAppClassLoader 的 parent
    if (p == null) {
        p = getSystemClassLoader();
    }
    this.parent = p;
                                                            // 3. 下面幾步是 獲取 Launcher.ExtClassLoader 賦值給 j2seClassLoader (主要是在類載入時會被用到)
    ClassLoader j = String.class.getClassLoader();
    if (j == null) {
        j = getSystemClassLoader();
        while (j.getParent() != null) {
            j = j.getParent();
        }
    }
    this.j2seClassLoader = j;                               // 4. 這裡進行賦值的就是 Launcher.ExtClassLoader

    securityManager = System.getSecurityManager();          // 5. 這裡的操作主要是判斷 Java 程式是否啟動安全策略
    if (securityManager != null) {
        refreshPolicy();
    }
}

8. WebappClassLoader start 方法

/**
 * Start the class loader.
 *
 * @exception LifecycleException if a lifecycle error occurs
 * 將 /WEB-INF/classes 及 /WEB-INF/lib 封裝成 URL 加入到 ClassLoader 的 URLClassPath 裡面
 */
@Override
public void start() throws LifecycleException {
                                                                            // 下面的 resources 其實就是  StandardRoot
                                                                            // WebappClassLoader 進行資源/類 URL 的載入操作 (/WEB-INF/classes  與 WEB-INF/lib 下面資源的 URL)
    WebResource classes = resources.getResource("/WEB-INF/classes");        // 1. 加入 /WEB_INF/classes 的 URL
    if (classes.isDirectory() && classes.canRead()) {
        addURL(classes.getURL());
    }                                                                       // 2. 加入 /WEB_INF/lib 下面的 jar 的URL 加入 URLClassPath
    WebResource[] jars = resources.listResources("/WEB-INF/lib");
    for (WebResource jar : jars) {
        if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
            addURL(jar.getURL());                                           // 3. 這一步就是將 ClassLoader需要載入的 classPath 路徑 加入到 URLClassLoader.URLClassPath 裡面
            jarModificationTimes.put(                                       // 4. 放一下 jar 檔案的 lastModified
                    jar.getName(), Long.valueOf(jar.getLastModified()));
        }
    }
}

這個方法其實就是將 /WEB-INF/classes 及 /WEB-INF/lib 封裝成 URL 加入到 ClassLoader 的 URLClassPath 裡面(PS: 當WebappClassloader在載入Class時, 通過這個URLs來決定是否載入 class )

9. WebappClassLoader modified方法

Tomcat 後來會啟用定時任務, 來檢查已經載入的資源是否有修改/增加/刪減, 來觸發 StandardContext 的 reload; 見程式碼

/**
 * Have one or more classes or resources been modified so that a reload
 * is appropriate?
 */
// 校驗 WebappClassLoader 載入的資源是否有修改過, 若有檔案修改過, 則進行熱部署
public boolean modified() {

    if (log.isDebugEnabled())
        log.debug("modified()");

    for (Entry<String,ResourceEntry> entry : resourceEntries.entrySet()) {       // 1. 遍歷已經載入的資源
        long cachedLastModified = entry.getValue().lastModified;
        long lastModified = resources.getClassLoaderResource(
                entry.getKey()).getLastModified();                                  // 2. 對比 file 的 lastModified的屬性
        if (lastModified != cachedLastModified) {                                   // 3. 若修改時間不對, 則說明檔案被修改過, StandardContext 需要重新部署
            if( log.isDebugEnabled() )
                log.debug(sm.getString("webappClassLoader.resourceModified",
                        entry.getKey(),
                        new Date(cachedLastModified),
                        new Date(lastModified)));
            return true;
        }
    }

    // Check if JARs have been added or removed
    WebResource[] jars = resources.listResources("/WEB-INF/lib");
    // Filter out non-JAR resources

    int jarCount = 0;
    for (WebResource jar : jars) {
        if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {      // 4. 比較 /WEB-INF/lib 下的 jar 包是否有修改/增加/減少
            jarCount++;                                                              // 5. 記錄 /WEB-INF/lib 下的 jar 的個數
            Long recordedLastModified = jarModificationTimes.get(jar.getName());
            if (recordedLastModified == null) {
                // Jar has been added
                log.info(sm.getString("webappClassLoader.jarsAdded",
                        resources.getContext().getName()));
                return true;
            }
            if (recordedLastModified.longValue() != jar.getLastModified()) {        // 6. 比較一下這次的檔案修改時間 與 上次檔案的修改時間是否一樣, 不一樣的話, 直接返回 true, StandardContext 需要重新部署
                // Jar has been changed
                log.info(sm.getString("webappClassLoader.jarsModified",
                        resources.getContext().getName()));
                return true;
            }
        }
    }

    if (jarCount < jarModificationTimes.size()){                                 // 7. 判斷 WebappClassloader檔案是夠有增加/減少, 若有變化的話, 直接返回 true, StandardContext 需要重新部署
        log.info(sm.getString("webappClassLoader.jarsRemoved",
                resources.getContext().getName()));
        return true;
    }


    // No classes have been modified
    return false;
}

10. WebappClassLoader loadClass方法

雙親委派模式的開關: WebappClassLoader 的loadClass有一個標識(delegateLoad) 用來控制是否啟用雙親委派模式;
下面來看方法的主要步驟:

 1. 判斷當前運用是否已經啟動, 未啟動, 則直接拋異常
 2. 呼叫 findLocaledClass0 從 resourceEntries 中判斷 class 是否已經載入 OK
 3. 呼叫 findLoadedClass(內部呼叫一個 native 方法) 直接檢視對應的 WebappClassLoader 是否已經載入過
 4. 呼叫 binaryNameToPath 判斷是否 當前 class 是屬於 J2SE 範圍中的, 若是的則直接通過 ExtClassLoader, BootstrapClassLoader 進行載入 (這裡是雙親委派)
 5. 在設定 JVM 許可權校驗的情況下, 呼叫 securityManager 來進行許可權的校驗(當前類是否有許可權載入這個類, 預設的許可權配置檔案是 ${catalina.base}/conf/catalina.policy)
 6. 判斷是否設定了雙親委派機制 或 當前 WebappClassLoader 是否能載入這個 class (通過 filter(name) 來決定), 將最終的值賦值給 delegateLoad
 7. 根據上一步中的 delegateLoad 來決定是否用 WebappClassloader.parent(也就是 sharedClassLoader) 來進行載入, 若載入成功, 則直接返回
 8. 上一步若未載入成功, 則呼叫 WebappClassloader.findClass(name) 來進行載入
 9. 若上一還是沒有載入成功, 則通過 parent 呼叫 Class.forName 來進行載入
 10. 若還沒載入成功的話, 那就直接拋異常

直接看程式碼:

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

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

    // Log access to stopped classloader                                     // 1.  判斷程式是否已經啟動了, 未啟動 OK, 就進行載入, 則直接拋異常
    if (!started) {
        try {
            throw new IllegalStateException();
        } catch (IllegalStateException e) {
            log.info(sm.getString("webappClassLoader.stopped", name), e);
        }
    }

    // (0) Check our previously loaded local class cache
                                                                             // 2. 當前物件快取中檢查是否已經載入該類, 有的話直接返回 Class
    clazz = findLoadedClass0(name);
    if (clazz != null) {
        if (log.isDebugEnabled())
            log.debug("  Returning class from cache");
        if (resolve)
            resolveClass(clazz);
        return (clazz);
    }

    // (0.1) Check our previously loaded class cache
                                                                             // 3. 是否已經載入過該類 (這裡的載入最終會呼叫一個 native 方法, 意思就是檢查這個 ClassLoader 是否已經載入過對應的 class 了哇)
    clazz = findLoadedClass(name);
    if (clazz != null) {
        if (log.isDebugEnabled())
            log.debug("  Returning class from cache");
        if (resolve)
            resolveClass(clazz);
        return (clazz);
    }

    // (0.2) Try loading the class with the system class loader, to prevent // 程式碼到這裡發現, 上面兩步是 1. 檢視 resourceEntries 裡面的資訊, 判斷 class 是否載入過, 2. 通過 findLoadedClass 判斷 JVM 中是否已經載入過, 但現在 直接用 j2seClassLoader(Luancher.ExtClassLoader 這裡的載入過程是雙親委派模式) 來進行載入
    //       the webapp from overriding J2SE classes                        // 這是為什麼呢 ? 主要是 這裡直接用 ExtClassLoader 來載入 J2SE 所對應的 class, 防止被 WebappClassLoader 載入了
    String resourceName = binaryNameToPath(name, false);                    // 4. 進行 class 名稱 轉路徑的操作 (檔案的尾綴是 .class)
    if (j2seClassLoader.getResource(resourceName) != null) {                // 5. 這裡的 j2seClassLoader 其實就是 ExtClassLoader, 這裡就是 查詢 BootstrapClassloader 與 ExtClassLoader 是否有許可權載入這個 class (通過 URLClassPath 來確認)
        try {
            clazz = j2seClassLoader.loadClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
    }

    // (0.5) Permission to access this class when using a SecurityManager   // 6. 這裡的 securityManager 與 Java 安全策略是否有關, 預設 (securityManager == null), 所以一開始看程式碼就不要關注這裡
    if (securityManager != null) {
        int i = name.lastIndexOf('.');
        if (i >= 0) {
            try {
                securityManager.checkPackageAccess(name.substring(0,i));   // 7. 通過 securityManager 對 是否能載入 name 的許可權進行檢查 (對應的策略都在 ${catalina.base}/conf/catalina.policy 裡面進行定義)
            } catch (SecurityException se) {
                String error = "Security Violation, attempt to use " +
                    "Restricted Class: " + name;
                log.info(error, se);
                throw new ClassNotFoundException(error, se);
            }
        }
    }

    boolean delegateLoad = delegate || filter(name);                      // 8. 讀取 delegate 的配置資訊, filter 主要判斷這個 class 是否能由這個 WebappClassLoader 進行載入 (false: 能進行載入, true: 不能被載入)

    // (1) Delegate to our parent if requested
    // 如果配置了 parent-first 模式, 那麼委託給父載入器                      // 9. 當進行載入 javax 下面的包 就直接交給 parent(sharedClassLoader) 來進行載入 (為什麼? 主要是 這些公共載入的資源統一由 sharedClassLoader 來進行載入, 能減少 Perm 區域的大小)
    if (delegateLoad) {                                                   // 10. 若 delegate 開啟, 優先使用 parent classloader( delegate 預設是 false); 這裡還有一種可能, 就是 經過 filter(name) 後, 還是返回 true, 那說明 WebappClassLoader 不應該進行載入, 應該交給其 parent 進行載入
        if (log.isDebugEnabled())
            log.debug("  Delegating to parent classloader1 " + parent);
        try {
            clazz = Class.forName(name, false, parent);                   // 11. 通過 parent ClassLoader 來進行載入 (這裡建構函式中第二個引數 false 表示: 使用 parent 載入 classs 時不進行初始化操作, 也就是 不會執行這個 class 中 static 裡面的初始操作 以及 一些成員變數ed賦值操作, 這一動作也符合 JVM 一貫的 lazy-init 策略)
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Loading class from parent");
                if (resolve)
                    resolveClass(clazz);
                return (clazz);                                           // 12. 通過 parent ClassLoader 載入成功, 則直接返回
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
    }

    // (2) Search local repositories
    if (log.isDebugEnabled())
        log.debug("  Searching local repositories");
    try {
        // 從 WebApp 中去載入類, 主要是 WebApp 下的 classes 目錄 與 lib 目錄
        clazz = findClass(name);                                         // 13. 使用當前的 WebappClassLoader 載入
        if (clazz != null) {
            if (log.isDebugEnabled())
                log.debug("  Loading class from local repository");
            if (resolve)
                resolveClass(clazz);
            return (clazz);
        }
    } catch (ClassNotFoundException e) {
        // Ignore
    }

    // (3) Delegate to parent unconditionally
    // 如果在當前 WebApp 中無法載入到, 委託給 StandardClassLoader 從 $catalina_home/lib 中去載入
    if (!delegateLoad) {                                                 // 14. 這是在 delegate = false 時, 在本 classLoader 上進行載入後, 再進行操作這裡
        if (log.isDebugEnabled())
            log.debug("  Delegating to parent classloader at end: " + parent);
        try {
            clazz = Class.forName(name, false, parent);                 // 15. 用 WebappClassLoader 的 parent(ExtClassLoader) 來進行載入
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Loading class from parent");
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
    }

    throw new ClassNotFoundException(name);                            // 16. 若還是載入不到, 那就丟擲異常吧
}

在上面步驟中, WebappClassLoader首選會在本地資源來獲取 class, 見方法 findLoadedClass0

protected Class<?> findLoadedClass0(String name) {                  // 1. 根據載入的 className 來載入 類

    String path = binaryNameToPath(name, true);                     // 2. 將 類名轉化成 類的全名稱

    ResourceEntry entry = resourceEntries.get(path);                // 3. resourceEntries 是 WebappClassLoader 載入好的 class 存放的地址
    if (entry != null) {
        return entry.loadedClass;                                   // 4. 將 載入好的 class 直接返回
    }
    return null;
}

10. WebappClassLoader findClassInternal方法

WebappClassLoader 作為ClassLoader 的子類, 其實現了自己的一套資源查詢方法, 具體的邏輯在 findClassInternal 中

protected Class<?> findClassInternal(String name)
    throws ClassNotFoundException {

    if (!validate(name))                                    // 1. 對於 J2SE 下面的 Class, 不能通過這個 WebappClassloader 來進行載入
        throw new ClassNotFoundException(name);

    String path = binaryNameToPath(name, true);             // 2. 將類名轉化成路徑名稱

    ResourceEntry entry = null;

    if (securityManager != null) {
        PrivilegedAction<ResourceEntry> dp =
            new PrivilegedFindResourceByName(name, path);
        entry = AccessController.doPrivileged(dp);
    } else {
        entry = findResourceInternal(name, path);          // 3. 呼叫 findResourceInternal  返回 class 的包裝類 entry
    }

    if (entry == null)
        throw new ClassNotFoundException(name);

    Class<?> clazz = entry.loadedClass;                    // 4. 若程式已經生成了 class, 則直接返回
    if (clazz != null)
        return clazz;

    synchronized (this) {
        clazz = entry.loadedClass;
        if (clazz != null)
            return clazz;

        if (entry.binaryContent == null)
            throw new ClassNotFoundException(name);

        // Looking up the package
        String packageName = null;
        int pos = name.lastIndexOf('.');
        if (pos != -1)
            packageName = name.substring(0, pos);         // 5. 獲取包名

        Package pkg = null;

        if (packageName != null) {
            pkg = getPackage(packageName);                // 6. 通過 包名 獲取對應的 Package 物件
            // Define the package (if null)
            if (pkg == null) {                            // 7. 若還不存在, 則definePackage
                try {
                    if (entry.manifest == null) {
                        definePackage(packageName, null, null, null, null,
                                null, null, null);
                    } else {
                        definePackage(packageName, entry.manifest,
                                entry.codeBase);
                    }
                } catch (IllegalArgumentException e) {
                    // Ignore: normal error due to dual definition of package
                }
                pkg = getPackage(packageName);            // 8. 獲取 Package
            }
        }

        if (securityManager != null) {                    // 9. 若程式執行配置了 securityManager, 則進行一些許可權方面的檢查

            // Checking sealing
            if (pkg != null) {
                boolean sealCheck = true;
                if (pkg.isSealed()) {
                    sealCheck = pkg.isSealed(entry.codeBase);
                } else {
                    sealCheck = (entry.manifest == null)
                        || !isPackageSealed(packageName, entry.manifest);
                }
                if (!sealCheck)
                    throw new SecurityException
                        ("Sealing violation loading " + name + " : Package "
                         + packageName + " is sealed.");
            }

        }

        try {                                            // 10 最終呼叫 ClassLoader.defineClass 來將 class 對應的 二進位制資料載入進來, 進行 "載入, 連線(解析, 驗證, 準備), 初始化" 操作, 最終返回 class 物件
            clazz = defineClass(name, entry.binaryContent, 0,                       
                    entry.binaryContent.length,
                    new CodeSource(entry.codeBase, entry.certificates));
        } catch (UnsupportedClassVersionError ucve) {
            throw new UnsupportedClassVersionError(
                    ucve.getLocalizedMessage() + " " +
                    sm.getString("webappClassLoader.wrongVersion",
                            name));
        }
        // Now the class has been defined, clear the elements of the local
        // resource cache that are no longer required.
        entry.loadedClass = clazz;
        entry.binaryContent = null;
        entry.codeBase = null;
        entry.manifest = null;
        entry.certificates = null;
        // Retain entry.source in case of a getResourceAsStream() call on
        // the class file after the class has been defined.
    }

    return clazz;                                         // 11. return 載入了的 clazz
}

11. WebappClassLoader findResourceInternal方法

在Tomcat中, 其資源的查詢都是通過 JNDI(具體儲存在了 StandardRoot裡面), WebappClassLoader 的資源查詢, 並且將找到的資源轉化成 byte[] 就是在 findResourceInternal 裡面實現

protected ResourceEntry findResourceInternal(final String name, final String path) {

    if (!started) {
        log.info(sm.getString("webappClassLoader.stopped", name));
        return null;
    }

    if ((name == null) || (path == null))
        return null;

    ResourceEntry entry = resourceEntries.get(path);        // 1. resourceEntries 裡面會儲存所有已經載入了的 檔案的資訊
    if (entry != null)
        return entry;

    boolean isClassResource = path.endsWith(CLASS_FILE_SUFFIX);

    WebResource resource = null;

    boolean fileNeedConvert = false;

    resource = resources.getClassLoaderResource(path);      // 2. 通過 JNDI 來進行查詢 資源 (想知道 resources 裡面到底是哪些資源, 可以看 StandardRoot 類)

    if (!resource.exists()) {                               // 3. 若資源不存在, 則進行返回
        return null;
    }

    entry = new ResourceEntry();                            // 4. 若所查詢的 class 對應的 ResourceEntry 不存在, 則進行構建一個
    entry.source = resource.getURL();
    entry.codeBase = entry.source;
    entry.lastModified = resource.getLastModified();

    if (needConvert) {
        if (path.endsWith(".properties")) {
            fileNeedConvert = true;
        }
    }

    /* Only cache the binary content if there is some content
     * available and either:
     * a) It is a class file since the binary content is only cached
     *    until the class has been loaded
     *    or
     * b) The file needs conversion to address encoding issues (see
     *    below)
     *
     * In all other cases do not cache the content to prevent
     * excessive memory usage if large resources are present (see
     * https://issues.apache.org/bugzilla/show_bug.cgi?id=53081).
     */
    if (isClassResource || fileNeedConvert) {                               // 5. 獲取對應資源的二進位制位元組流, 當需要進行轉碼時, 進行相應的轉碼操作
        byte[] binaryContent = resource.getContent();
        if (binaryContent != null) {
             if (fileNeedConvert) {
                // Workaround for certain files on platforms that use
                // EBCDIC encoding, when they are read through FileInputStream.
                // See commit message of rev.303915 for details
                // http://svn.apache.org/viewvc?view=revision&revision=303915
                String str = new String(binaryContent);
                try {
                    binaryContent = str.getBytes(StandardCharsets.UTF_8);   // 6. 進行資源轉碼為 UTF-8
                } catch (Exception e) {
                    return null;
                }
            }
            entry.binaryContent = binaryContent;                           // 7. 獲取資源對應的 二進位制資料資訊
            // The certificates and manifest are made available as a side
            // effect of reading the binary content
            entry.certificates = resource.getCertificates();               // 8. 獲取資源的證書
        }
    }
    entry.manifest = resource.getManifest();

    if (isClassResource && entry.binaryContent != null &&
            this.transformers.size() > 0) {
        // If the resource is a class just being loaded, decorate it
        // with any attached transformers
        String className = name.endsWith(CLASS_FILE_SUFFIX) ?
                name.substring(0, name.length() - CLASS_FILE_SUFFIX.length()) : name;
        String internalName = className.replace(".", "/");

        for (ClassFileTransformer transformer : this.transformers) {
            try {
                byte[] transformed = transformer.transform(
                        this, internalName, null, null, entry.binaryContent
                );
                if (transformed != null) {
                    // 設定 二進位制設定到 ResourceEntry
                    entry.binaryContent = transformed;
                }
            } catch (IllegalClassFormatException e) {
                log.error(sm.getString("webappClassLoader.transformError", name), e);
                return null;
            }
        }
    }

    // Add the entry in the local resource repository
    synchronized (resourceEntries) {                                        // 9. 將生成的 entry 放入 resourceEntries 中
        // Ensures that all the threads which may be in a race to load
        // a particular class all end up with the same ResourceEntry
        // instance
        ResourceEntry entry2 = resourceEntries.get(path);
        if (entry2 == null) {
            // 向本地資源快取註冊 ResourceEntry
            resourceEntries.put(path, entry);
        } else {
            entry = entry2;
        }
    }

    return entry;
}

到這裡 WebappClassLoadere.loadClass 的邏輯已經差不多了, 好像 這個WebappClassLoader 的設計一般般啊! 其實還沒完, WebappClassLoader 裡面設計最精彩的其實是它的stop方法裡面對各種資源的清除;

11. WebappClassLoader stop 方法

在進行熱部署/重部署時, 會呼叫 WebappClassLoader 的 stop 方法, 它主要做了下面四種資源的清除

public void stop() throws LifecycleException {

    // Clearing references should be done before setting started to
    // false, due to possible side effects
    clearReferences();              // 1. 清除各種資源

    started = false;

    resourceEntries.clear();        // 2. 清空各種 WebappClassLoader 載入的資料
    jarModificationTimes.clear();   // 3. 清空各種 監視的資源(監視的資源一旦有變動, 就會觸發 StandardContext 的重新載入機制)
    resources = null;

    permissionList.clear();         // 4. 下面兩個清空的是與 Java 許可權相關的資源
    loaderPC.clear();
}

而其中最複雜的要數 clearReferences 了;

protected void clearReferences() {

    // De-register any remaining JDBC drivers
    clearReferencesJdbc();                         // 1. 清除應用連結的資料來源 (呼叫 JdbcLeakPrevention.clearJdbcDriverRegistrations 來獲取所有 這個 WebappClassLoader 加載出來的 JDBC 驅動, 並且呼叫 DriverManager.deregisterDriver 登出掉)

    // Stop any threads the web application started
    clearReferencesThreads();                      // 2. 清除應用啟動的執行緒 (通過執行緒組獲取所有存活的執行緒, 針對 Timer 執行緒, 在清空其內部 queue 後, 通過反射呼叫 cancel 來停止Timer; 若是 ThreadPoolExecutor 裡面的執行緒則直接呼叫其 shutdownNow() 方法來關閉整個執行緒池)

    // Check for leaks triggered by ThreadLocals loaded by this class loader
    checkThreadLocalsForLeaks();                   // 3. 清除 ThreadLocal 快取

    // Clear RMI Targets loaded by this class loader
    clearReferencesRmiTargets();                   // 4. 清除 rmiTarget (還是通過反射, 拿到rmi 裡面的資源)

    // Null out any static or final fields from loaded classes,
    // as a workaround for apparent garbage collection bugs
    if (clearReferencesStatic) {
        clearReferencesStaticFinal();              // 5. static, final 資源清空 (這裡就是遍歷 WebappClassLoader 加載出來的 class,將其中 static, final 的field 置為null, 加速 GC)
    }

     // Clear the IntrospectionUtils cache.
    IntrospectionUtils.clear();                    // 6. 反射資源清空 (IntrospectionUtils.objectMethods 裡面快取這所有呼叫它的 class 及method 等資訊)

    // Clear the classloader reference in common-logging
    if (clearReferencesLogFactoryRelease) {       // 7. 日誌工廠釋放(主要是讓 ClassLoaderLogManager.ClassLoaderLogInfo 中的 handles 從 logger 裡面清除, 見 ClassLoaderLogManager.reset() 方法)
        org.apache.juli.logging.LogFactory.release(this);
    }

    // Clear the resource bundle cache
    // This shouldn't be necessary, the cache uses weak references but
    // it has caused leaks. Oddly, using the leak detection code in
    // standard host allows the class loader to be GC'd. This has been seen
    // on Sun but not IBM JREs. Maybe a bug in Sun's GC impl?
    clearReferencesResourceBundles();             // 8. 資源繫結解除 (清除掉 ResourceBundle 裡面的快取集合 cacheList, 其實清不清除沒關係, 因為 LoaderReference 是對 classloader 的一個弱引用, 在沒有強引用的情況下, 弱引用的物件馬上會被回收掉)

    // Clear the classloader reference in the VM's bean introspector
    java.beans.Introspector.flushCaches();        // 9. 清空快取 (其實就是清空 Introspector 裡面快取 類 方法的 declaredMethodCache)

    // Clear any custom URLStreamHandlers
    TomcatURLStreamHandlerFactory.release(this);  // 10.這個運用額場景比較少, 主要刪除 由 當前 WebappClassLoader 加載出來的 URLStreamHandlerFactory
}

好多啊! 一個WebappClassLoader.stop方法觸發了Tomcat做這麼多事情, 那我們回過來想一下, 為什麼 Tomcat 要做這麼多事情

1. 加速 StandardContext 所對應的資源GC
2. 防止WebappClassLoader leaking, 從而導致 WebAppClassLoader所載入的所有資源都洩露, 最終導致記憶體洩露

知識點:
Object <---引用---> Class <---引用---> ClassLoader
類載入器加載出來的類或物件, 對類載入器有引用, 既然

出現記憶體洩露的主要是下面這幾種情況:

1. DriverManager(由 BootstrapClassloader載入) 引用 jdbc (由 WebAppClassLoader載入)
    DriverManager 是由 BootstrapClassloader載入, 所以其永遠不會被GC, 當是它有引用了由 WebAppClassLoader 載入的 JDBC, 所以導致 JDBC 與 WebAppClassLoader 都被引用住, 
    而 WebAppClassLoader 又對由其載入的類有引用, 所以由 WebAppClassLoader 載入的類都不會被GC, 最終在多次 StandardContext.reload 後就出現記憶體洩露
2. ThreadLocal
    Tomcat 的工作執行緒池裡面執行緒可能很長時間才會死掉, 而ThreadLocalMap的生命週期由和 Thread的一樣, 這樣導致 ThreadLocalMap 裡面的 Value 也被引用住, 
    而這個 Valve很有可能是 StandardContext.WebappClassloader 載入, 所以就又導致 WebappClassloader 被引用, 而 WebAppClassLoader 又對由其載入的類有引用, 
    所以由 WebAppClassLoader 載入的類都不會被GC, 最終在多次 StandardContext.reload 後就出現記憶體洩露
    這時我們就將執行緒裡面的 ThreadLocalMap 裡面的值清掉就 OK 了
3. 由WebappClassloader加載出來的執行緒一直執行
    這個簡單, 通過 threadGroup 獲取所有Thread, 判斷 contextClassLoader 是否是 WebAppClassLoader, 若是的話直接殺掉
4. IntrospectionUtils
    IntrospectionUtils 是 Tomcat 的反射工具類, 這裡也清空一下快取的資料, 防止又出現 WebappClassLoader 又被引導, 從而導致 記憶體洩露 

12. WebappClassLoader 防止記憶體洩露之JDBC

在JDBC上, Tomcat 主要是通過反射呼叫 JdbcLeakPrevention 來實現的

HashSet<Driver> originalDrivers = new HashSet<>();
Enumeration<Driver> drivers = DriverManager.getDrivers();
while (drivers.hasMoreElements()) {
    originalDrivers.add(drivers.nextElement());
}
drivers = DriverManager.getDrivers();
while (drivers.hasMoreElements()) {
    Driver driver = drivers.nextElement();
    // Only unload the drivers this web app loaded
    if (driver.getClass().getClassLoader() !=
        this.getClass().getClassLoader()) {
        continue;
    }
    // Only report drivers that were originally registered. Skip any
    // that were registered as a side-effect of this code.
    /**
     * 實際就是 DriverManager能拿到所有的 Driver的一個集合, 然後判斷 Driver該類是否是由當前應用
     * 類載入器進行載入的, 如果是的話, 直接呼叫 DriverManager.deregisterDriver() 對其進行解除安裝
     */
    if (originalDrivers.contains(driver)) {
        driverNames.add(driver.getClass().getCanonicalName());
    }
    DriverManager.deregisterDriver(driver);
}

這裡的操作也很簡單, 就是將所有註冊了的 JDBC_Driver拿出來, 進行 deregister掉

12. WebappClassLoader 防止記憶體洩露之 Thread

步驟:

1. 通過ThreadGroup獲取所有的執行緒
2. 判斷獲取的執行緒's ContextClassLoader 是否是當前的 WebappClassLoader, 若是的話直接關了(PS: 若是執行緒池裡面的執行緒, 直接呼叫執行緒池的 stop)

見程式碼:

 /* 通過 StandardContext 的幾個屬性來控制是否 clear掉當前應用創建出來的執行緒
 * 主要思路:
 * 首先通過 當前的ThreadGroup來拿到 ThreadGroup來拿到當前Tomcat啟動(也就是JVM虛擬機器)的所有執行緒
 * 拿到之後對比當前 Thread.contextClassLoader 是否就是當前應用的 webappClassLoader, 如果一樣, 說明 Thread
 * 就是當前應用創建出來的執行緒. 之後 Tomcat 針對 JVM 的執行緒, Timer執行緒, JDK執行緒池 ThreadExecutor中建立的執行緒等多種型別的執行緒, 給出其對應的辦法
 */
@SuppressWarnings("deprecation") // thread.stop()
private void clearReferencesThreads() {
    
    Thread[] threads = getThreads();                             // 1. getThreads 返回的是一個 JVM 例項中所有的執行緒數, 而我們處理的執行緒是 由當前 WebappClassLoader 加載出來的 執行緒
    List<Thread> executorThreadsToStop = new ArrayList<>();

    // Iterate over the set of threads
    for (Thread thread : threads) {
        if (thread != null) {
            ClassLoader ccl = thread.getContextClassLoader();
            if (ccl == this) {                                  // 2. 判斷當前執行緒是否是由當前 WebappClassLoader 加載出來的
                // Don't warn about this thread
                if (thread == Thread.currentThread()) {
                    continue;
                }

                // JVM controlled threads
                // 對於 JVM 執行緒 保留
                ThreadGroup tg = thread.getThreadGroup();
                if (tg != null &&                               // 3. 對應 RMI 或 system 的
                        JVM_THREAD_GROUP_NAMES.contains(tg.getName())) {
                    /**
                     * 對於 keeperalive的Timer執行緒, 應該由
                     * keeperalive自己的心跳自己結束, 不應該在
                     * 這裡強制關掉, 因此這裡將該 Thread 交給
                     * 其 classloader的上級, 讓其自動掃描後關掉
                     */
                    // HttpClient keep-alive threads
                    if (clearReferencesHttpClientKeepAliveThread &&
                            thread.getName().equals("Keep-Alive-Timer")) {
                        thread.setContextClassLoader(parent);
                        log.debug(sm.getString(
                                "webappClassLoader.checkThreadsHttpClient"));
                    }

                    // Don't warn about remaining JVM controlled threads
                    continue;
                }

                // Skip threads that have already died
                // 看看執行緒是否還存活
                if (!thread.isAlive()) {                          // 4. 若執行緒已經不存活, 則直接 continue
                    continue;
                }

                // TimerThread can be stopped safely so treat separately
                // "java.util.TimerThread" in Sun/Oracle JDK
                // "java.util.Timer$TimerImpl" in Apache Harmony and in IBM JDK
                if (thread.getClass().getName().startsWith("java.util.Timer") &&
                        clearReferencesStopTimerThreads) {
                    clearReferencesStopTimerThread(thread);       // 5. 定時執行緒 Timer 通過 反射清空其內部的 queue, 並且呼叫 cancel 來 stop 掉
                    continue;
                }

                if (isRequestThread(thread)) {                   // 6. 檢測是請求執行緒的話保持不動 (如何判斷出來呢, 呵呵 直接通過堆疊資訊獲取)
                    log.error(sm.getString("webappClassLoader.warnRequestThread",
                            getContextName(), thread.getName()));
                } else {
                    log.error(sm.getString("webappClassLoader.warnThread",
                            getContextName(), thread.getName()));
                }

                // Don't try an stop the threads unless explicitly
                // configured to do so
                // 設定 clearReferencesStopThreads = false 直接 continue
                if (!clearReferencesStopThreads) {
                    continue;
                }

                // If the thread has been started via an executor, try
                // shutting down the executor
                boolean usingExecutor = false;                  // 7. 若是通過執行緒池來啟動的執行緒, 則直接呼叫執行緒池的 shutdownNow 來進行停止執行緒池
                try {

                    // Runnable wrapped by Thread
                    // "target" in Sun/Oracle JDK
                    // "runnable" in IBM JDK
                    // "action" in Apache Harmony
                    Object target = null;
                    for (String fieldName : new String[] { "target",
                            "runnable", "action" }) {
                        try {
                            Field targetField = thread.getClass()
                                    .getDeclaredField(fieldName);
                            targetField.setAccessible(true);
                            target = targetField.get(thread);
                            break;
                        } catch (NoSuchFieldException nfe) {
                            continue;
                        }
                    }

                    // "java.util.concurrent" code is in public domain,
                    // so all implementations are similar
                    if (target != null &&                                       // 8. 若是執行緒池裡面的執行緒, 則直接呼叫 ThreadPoolExecutor.shutdownNow()
                            target.getClass().getCanonicalName() != null
                            && target.getClass().getCanonicalName().equals(
                            "java.util.concurrent.ThreadPoolExecutor.Worker")) {
                        Field executorField =
                            target.getClass().getDeclaredField("this$0");       // 9. 獲取執行緒池
                        executorField.setAccessible(true);
                        Object executor = executorField.get(target);
                        if (executor instanceof ThreadPoolExecutor) {
                            ((ThreadPoolExecutor) executor).shutdownNow();      // 10. 停止執行緒池
                            usingExecutor = true;
                        }
                    }
                } catch (SecurityException e) {
                    log.warn(sm.getString(
                            "webappClassLoader.stopThreadFail",
                            thread.getName(), getContextName()), e);
                } catch (NoSuchFieldException e) {
                    log.warn(sm.getString(
                            "webappClassLoader.stopThreadFail",
                            thread.getName(), getContextName()), e);
                } catch (IllegalArgumentException e) {
                    log.warn(sm.getString(
                            "webappClassLoader.stopThreadFail",
                            thread.getName(), getContextName()), e);
                } catch (IllegalAccessException e) {
                    log.warn(sm.getString(
                            "webappClassLoader.stopThreadFail",
                            thread.getName(), getContextName()), e);
                }

                if (usingExecutor) {
                    // Executor may take a short time to stop all the
                    // threads. Make a note of threads that should be
                    // stopped and check them at the end of the method.
                                                                                 // 11. 如果是 ThreadPoolExecutor.shutdownNow 需要一段時間才能停止下來, 將執行緒加入到 executorThreadsToStop, 接下來一個一個遍歷執行緒, 若執行緒還存活, 則直接呼叫執行緒的 stop 方法
                    executorThreadsToStop.add(thread);
                } else {
                    // This method is deprecated and for good reason. This
                    // is very risky code but is the only option at this
                    // point. A *very* good reason for apps to do this
                    // clean-up themselves.
                    thread.stop();
                }
            }
        }
    }

    // If thread stopping is enabled, executor threads should have been
    // stopped above when the executor was shut down but that depends on the
    // thread correctly handling the interrupt. Give all the executor
    // threads a few seconds shutdown and if they are still running
    // Give threads up to 2 seconds to shutdown
    int count = 0;
    for (Thread t : executorThreadsToStop) {                                    // 12. 確保執行緒是否全部都 stop 掉了
        while (t.isAlive() && count < 100) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                // Quit the while loop
                break;
            }
            count++;
        }
        if (t.isAlive()) {
            // This method is deprecated and for good reason. This is
            // very risky code but is the only option at this point.
            // A *very* good reason for apps to do this clean-up
            // themselves.
            t.stop();                                                           // 13. 若執行緒還存活, 則最後執行 stop
        }
    }
}

13. WebappClassLoader 防止記憶體洩露之 ThreadLocal

針對ThreadLocal的記憶體洩露, 我們來看看 Tomcat 是這麼做的

 /* AppClassLoader -> 工作執行緒 Thread A -> Thread A.ThreadLocalMap -> Thread A.ThreadLocalMap.value (若這個 value 是 WebappClassLoader 載入的話), 那麼 WebappClassLoader也就被強引用, WepappClassLoader 也就不能被解除安裝
 *
 */
private void checkThreadLocalsForLeaks() {
    Thread[] threads = getThreads();

    try {
        // Make the fields in the Thread class that store ThreadLocals
        // accessible
        Field threadLocalsField =
            Thread.class.getDeclaredField("threadLocals");                 // 1. 當前執行緒快取的資料
        threadLocalsField.setAccessible(true);
        Field inheritableThreadLocalsField =
            Thread.class.getDeclaredField("inheritableThreadLocals");    // 2. 當前執行緒建立時, 繼承父執行緒下來的 ThreadLocalMap 裡面的資料
        inheritableThreadLocalsField.setAccessible(true);
        // Make the underlying array of ThreadLoad.ThreadLocalMap.Entry objects
        // accessible
        Class<?> tlmClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap");
        Field tableField = tlmClass.getDeclaredField("table");
        tableField.setAccessible(true);
        Method expungeStaleEntriesMethod = tlmClass.getDeclaredMethod("expungeStaleEntries");   // 3. expungeStaleEntries 這個方法只能刪除 key 是 null 的 Entry
        expungeStaleEntriesMethod.setAccessible(true);

        for (int i = 0; i < threads.length; i++) {
            Object threadLocalMap;
            if (threads[i] != null) {

                // Clear the first map
                threadLocalMap = threadLocalsField.get(threads[i]);
                if (null != threadLocalMap){
                    expungeStaleEntriesMethod.invoke(threadLocalMap);                       // 4. expunge (擦去), stale (陳腐的) 其實就是刪除 threadLocalMap 裡面 key 是 null 的 Entry
                    checkThreadLocalMapForLeaks(threadLocalMap, tableField);                // 5. 這裡只是判斷是否 有可能引起記憶體洩露, 是的話, 就列印一下日誌 (這裡 我們其實可以參考 ThreadLocalLeakPreventionListener, 將執行緒池裡的所有執行緒 renew/stop )
                }

                // Clear the second map
                threadLocalMap =inheritableThreadLocalsField.get(threads[i]);
                if (null != threadLocalMap){
                    expungeStaleEntriesMethod.invoke(threadLocalMap);                       // 6. 刪除 inheritableThreadLocals 裡面 key 是 null 的 Entry
                    checkThreadLocalMapForLeaks(threadLocalMap, tableField);                // 7. 這裡只是判斷是否 有可能引起記憶體洩露, 是的話, 就列印一下日誌 (這裡 我們其實可以參考 ThreadLocalLeakPreventionListener, 將執行緒池裡的所有執行緒 renew/stop )
                }
            }
        }
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        log.warn(sm.getString(
                "webappClassLoader.checkThreadLocalsForLeaksFail",
                getContextName()), t);
    }
}

我們看到 程式碼中只是列印了一下, 什麼, 這叫什麼防止記憶體洩露, 等等, 我們回頭想想, ThreadLocalMap 的生命週期與 thread 一樣, 那上面已經清除了由 WebappClassLoader 載入的執行緒不就沒事了, OK! (PS: 不對啊, 那還有存在於 Tomcat 工作執行緒池中的執行緒的 ThreadLocalMap, 這不是一樣導致洩露.......?, 對, 是會導致洩露的, 而且 Tomcat 確實沒進行處理 (~))

14. 總結

現在再回頭看看開篇提出的問題, 現在我們有了答案了, 先看 Tomcat classLoader 設計的優點吧!

1. 熱部署功能或專案(PS: 熱部署JSP, Context)
2. 隔離資源的訪問
    (1) 不同的 Context 之間不能相互訪問對方載入的資源, 舉例: 可能Context1用Spring3.1, 而 Context2用Spring4.1 若用同一個Classloader 則遇到 spring 的class只能載入一份, 就會出現想用 spring4.1裡面的 AnnotationUtils, 但是 classLoader 其實載入的是 spring 3.1裡面的類, 這樣很有可能出現 NoSuchMethodError 異常
    (2) 不讓 Context 載入類不能訪問到 Tomcat 容器自身的類

但我們再想想, 為了一個熱部署, Tomcat 在Stop方法裡面做了多少的清理工作, 而在真實產線上 很少用Tomcat的reload, 為啥? 就是我們寫的程式有時會做些Tomcat始料不及的事情 (比如 自己建立一些ClassLoader, 再用這個 ClassLoader 開啟一個 loop, loop裡面有引用 WebappClassLoader 加載出來的資料, 想想就覺得害怕....), 這樣的話 Tomcat, 就不能完全清理所有的資源, 最終在 幾次 StandardContext.reload 的情況下, Tomcat最終因為記憶體溢位而掛了!

15. 參考:

lesson3-jvm虛擬機器類載入
Java ClassLoader學習二:ClassLader原始碼
Tomcat 7.0 原理與原始碼分析
Java ClassLoader學習一:Launcher原始碼
classloader使用與原理分析
Tomcat7.0原始碼分析——類載入體系
Tomcat 架構解析