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 架構解析

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容