那些有趣的程式碼(二)–偏不聽父母話的 Tomcat 類載入器
- 2019 年 10 月 30 日
- 筆記
看 Tomcat 的源碼越看越有趣。Tomcat 的程式碼總有一種處處都有那麼一點調皮的感覺。今天就聊一聊 Tomcat 的類載入機制。
了解過 JVM 的類載入一定知道,JVM 類載入的雙親委派機制。但是 Tomcat 卻打破了 JVM 固有的雙親委派載入機制。
JVM 的類載入
首先需要明確一下類載入是什麼?
- Java虛擬機把描述類的數據從Class文件載入到記憶體,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的載入機制。
JVM 預定義的三個載入器:
- 啟動類載入器(Bootstrap ClassLoader):是用本地程式碼實現的類裝入器,它負責將
/lib
下面的類庫載入到記憶體中(比如rt.jar
)。由於引導類載入器涉及到虛擬機本地實現細節,開發者無法直接獲取到啟動類載入器的引用,所以不允許直接通過引用進行操作。 - 標準擴展類載入器(Extension ClassLoader):是由 Sun 的
ExtClassLoader(sun.misc.Launcher$ExtClassLoader)
實現的。它負責將< Java_Runtime_Home >/lib/ext
或者由系統變數java.ext.dir
指定位置中的類庫載入到記憶體中。開發者可以直接使用標準擴展類載入器。 - 應用程式類載入器(Application ClassLoader):是由 Sun 的
AppClassLoader(sun.misc.Launcher$AppClassLoader)
實現的。它負責將系統類路徑(CLASSPATH
)中指定的類庫載入到記憶體中。開發者可以直接使用系統類載入器。
雙親委派機制:
所謂雙親委派機制,這裡要指出的是,其實雙親委派來源於英文的 」parents delegate「,僅僅表示的只是」父輩「,可見翻譯的人不但英文是半吊子,而且也不了解 JVM 的類載入策略,造成了很大的誤解。尤其是這個」雙「字在初學的時候給我造成了極大的干擾。所以換個說法,應該是」父輩代理「。
類載入的時候,把載入的這個動作遞歸的委託給父輩,由父輩代勞,只有父輩無法載入時,才會由自己載入。
雙親委派載入模型:

parents
這裡需要特別注意的是載入器的關係並非是繼承的關係。我們看程式碼:
static class ExtClassLoader extends URLClassLoader{ ... ... } static class AppClassLoader extends URLClassLoader{ ... ... }
二者同時繼承了 URLClassLoader ,繼承關係如下:

appClassLoader
怎麼實現委託機制呢?在 ClassLoader 裡面有幾處比較重要的程式碼:
public abstract class ClassLoader { // The parent class loader for delegation // Note: VM hardcoded the offset of this field, thus all new fields // must be added *after* it. private final ClassLoader parent; protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { // 嘗試使用 父輩的 loadClass 方法 c = parent.loadClass(name, false); } else { // 如果沒有 父輩的 classLoader 就使用 bootstrap classLoader c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); // 父輩沒法載入這個 class,就自己嘗試載入 c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } } // 根據類名 尋找 class。我們在之前我們講過,不通過的 classLoader 載入的 class 的位置不同。 protected Class<?> findClass(String name) throws ClassNotFoundException { return defineClass(name, res); } }
- 首先在初始化 ClassLoader 的時候需要指定自己的 parent 是誰?(這很重要)
- 先檢查類有沒被載入,如果類已經被載入了,直接返回。
- 如果沒有被載入,則通過 parent 的 loadClass 來嘗試載入類。(雙親委派的核心邏輯)
- 找不到 parent 的時候使用 bootstrap ClassLoader 進行載入。
- 如果委託的 parent 沒法載入類,那就自己載入。
Tomcat 的類載入
Tomcat 自己實現了自己的類載入器 WebAppClassLoader。類圖關係圖如下:

WebAppClassLoader
我們就來看看 Tomcat 的類載入器是怎麼打破雙親委派的機制的。我們先看程式碼:
findClass
@Override public Class<?> findClass(String name) throws ClassNotFoundException { // Ask our superclass to locate this class, if possible // (throws ClassNotFoundException if it is not found) Class<?> clazz = null; // 先在自己的 Web 應用目錄下查找 class clazz = findClassInternal(name); // 找不到 在交由父類來處理 if ((clazz == null) && hasExternalRepositories) { clazz = super.findClass(name); } if (clazz == null) { throw new ClassNotFoundException(name); } return clazz; }
對於 Tomcat 的類載入的 findClass 方法:
- 首先在 web 目錄下查找。(重要)
- 找不到再交由父類的 findClass 來處理。
- 都找不到,那就拋出 ClassNotFoundException。
loadClass 方法
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> clazz = null; //1. 先在本地cache查找該類是否已經載入過 clazz = findLoadedClass0(name); if (clazz != null) { if (resolve) resolveClass(clazz); return clazz; } //2. 從系統類載入器的cache中查找是否載入過 clazz = findLoadedClass(name); if (clazz != null) { if (resolve) resolveClass(clazz); return clazz; } // 3. 嘗試用ExtClassLoader類載入器類載入 ClassLoader javaseLoader = getJavaseClassLoader(); try { clazz = javaseLoader.loadClass(name); if (clazz != null) { if (resolve) resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { // Ignore } // 4. 嘗試在本地目錄搜索class並載入 try { clazz = findClass(name); if (clazz != null) { if (resolve) resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { // Ignore } // 5. 嘗試用系統類載入器(也就是AppClassLoader)來載入 try { clazz = Class.forName(name, false, parent); if (clazz != null) { if (resolve) resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { // Ignore } } //6. 上述過程都載入失敗,拋出異常 throw new ClassNotFoundException(name); }
總結一下載入的步驟:
- 先在本地cache查找該類是否已經載入過,看看 Tomcat 有沒有載入過這個類。
- 如果Tomcat 沒有載入過這個類,則從系統類載入器的cache中查找是否載入過。
- 如果沒有載入過這個類,嘗試用ExtClassLoader類載入器類載入,重點來了,這裡並沒有首先使用 AppClassLoader 來載入類。這個Tomcat 的 WebAPPClassLoader 違背了雙親委派機制,直接使用了 ExtClassLoader來載入類。這裡注意 ExtClassLoader 雙親委派依然有效,ExtClassLoader 就會使用 Bootstrap ClassLoader 來對類進行載入,保證了 Jre 裡面的核心類不會被重複載入。比如在 Web 中載入一個 Object 類。WebAppClassLoader → ExtClassLoader → Bootstrap ClassLoader,這個載入鏈,就保證了 Object 不會被重複載入。
- 如果 BoostrapClassLoader,沒有載入成功,就會調用自己的 findClass 方法由自己來對類進行載入,findClass 載入類的地址是自己本 web 應用下的 class。
- 載入依然失敗,才使用 AppClassLoader 繼續載入。
- 都沒有載入成功的話,拋出異常。
總結一下以上步驟,WebAppClassLoader 載入類的時候,故意打破了JVM 雙親委派機制,繞開了 AppClassLoader,直接先使用 ExtClassLoader 來載入類。
- 保證了基礎類不會被同時載入。
- 由保證了在同一個 Tomcat 下不同 web 之間的 class 是相互隔離的。
more
準備把有趣的程式碼這個系列慢慢寫下去,發現編程的樂趣: