1. 程式人生 > >Tomcat 第六篇:類載入機制

Tomcat 第六篇:類載入機制

![](https://cdn.geekdigging.com/java/tomcat/tomcat_header.jpg) ## 1. 引言 Tomcat 在部署 Web 應用的時候,是將應用放在 webapps 資料夾目錄下,而 webapps 對應到 Tomcat 中是容器 Host ,裡面的資料夾則是對應到 Context ,在 Tomcat 啟動以後, webapps 中的所有的 Web 應用都可以提供服務。 這裡會涉及到一個問題, webapps 下面不止會有一個應用,比如有 APP1 和 APP2 兩個應用,它們分別有自己獨立的依賴 jar 包,這些 jar 包會位於 APP 的 WEB-INFO/lib 這個目錄下,這些 jar 包大概率是會有重複的,比如常用的 Spring 全家桶,在這裡面,版本肯定會有不同,那麼 Tomcat 是如何處理的? ## 2. JVM 類載入機制 說到 Tomcat 的類載入機制,有一個繞不開的話題是 JVM 是如何進行類載入的,畢竟 Tomcat 也是執行在 JVM 上的。 以下內容參考自周志明老師的 「深入理解 Java 虛擬機器」。 ### 2.1 什麼是類的載入 類的載入指的是將類的 .class 檔案中的二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,然後在堆區建立一個 java.lang.Class 物件,用來封裝類在方法區內的資料結構。類的載入的最終產品是位於堆區中的 Class 物件, Class 物件封裝了類在方法區內的資料結構,並且向 Java 程式設計師提供了訪問方法區內的資料結構的介面。 類載入器並不需要等到某個類被 「首次主動使用」 時再載入它, JVM 規範允許類載入器在預料某個類將要被使用時就預先載入它,如果在預先載入的過程中遇到了 .class 檔案缺失或存在錯誤,類載入器必須在程式首次主動使用該類時才報告錯誤( LinkageError 錯誤)如果這個類一直沒有被程式主動使用,那麼類載入器就不會報告錯誤。 ```shell 載入.class檔案的方式 – 從本地系統中直接載入 – 通過網路下載.class檔案 – 從zip,jar等歸檔檔案中載入.class檔案 – 從專有資料庫中提取.class檔案 – 將Java原始檔動態編譯為.class檔案 ``` ### 2.2 類生命週期 接下來,我們看下一個類的生命週期: 一個型別從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期將會經歷載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和解除安裝(Unloading)七個階段,其中驗證、準備、解析三個部分統稱為連線(Linking)。 ![](https://cdn.geekdigging.com/java/tomcat/tomcat6/java_class_life_cycle.png) ### 2.3 雙親委派模型 Java 提供三種類型的系統類載入器: - 啟動類載入器(Bootstrap ClassLoader):由 C++ 語言實現,屬於 JVM 的一部分,其作用是載入 `\lib` 目錄中的檔案,或者被 `-Xbootclasspath` 引數所指定的路徑中的檔案,並且該類載入器只加載特定名稱的檔案(如 rt.jar ),而不是該目錄下所有的檔案。啟動類載入器無法被 Java 程式直接引用。 - 擴充套件類載入器( Extension ClassLoader ):由 `sun.misc.Launcher.ExtClassLoader` 實現,它負責載入 `\lib\ext` 目錄中的,或者被 `java.ext.dirs` 系統變數所指定的路徑中的所有類庫,開發者可以直接使用擴充套件類載入器。 - 應用程式類載入器( Application ClassLoader ):也稱系統類載入器,由 `sun.misc.Launcher.AppClassLoader` 實現。負責載入使用者類路徑( Class Path )上所指定的類庫,開發者可以直接使用這個類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。 ![](https://cdn.geekdigging.com/java/tomcat/tomcat6/jvm_class_loader.png) **雙親委派模型的工作機制:** 如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。 **為什麼?** 例如類 java.lang.Object ,它存放在 rt.jar 之中。無論哪一個類載入器都要載入這個類。最終都是雙親委派模型最頂端的 Bootstrap 類載入器去載入。因此 Object 類在程式的各種類載入器環境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類載入器自行去載入的話,如果使用者編寫了一個稱為 「java.lang.Object」 的類,並存放在程式的 ClassPath 中,那系統中將會出現多個不同的 Object 類, java 型別體系中最基礎的行為也就無法保證,應用程式也將會一片混亂。 ## 3. Tomcat 類載入機制 先整體看下 Tomcat 類載入器: ![](https://cdn.geekdigging.com/java/tomcat/tomcat6/tomcat_class_loader.png) 可以看到,在原來的 JVM 的類載入機制上面, Tomcat 新增了幾個類載入器,包括 3 個基礎類載入器和每個 Web 應用的類載入器。 3 個基礎類載入器在 `conf/catalina.properties` 中進行配置: ```shell common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar" server.loader= shared.loader= ``` - Common: 以應用類載入器為父類,是 Tomcat 頂層的公用類載入器,其路徑由 `conf/catalina.properties` 中的 `common.loader` 指定,預設指向 `${catalina.home}/lib` 下的包。 - Catalina: 以 Common 類載入器為父類,是用於載入 Tomcat 應用伺服器的類載入器,其路徑由 `server.loader` 指定,預設為空,此時 Tomcat 使用 Common 類載入器載入應用伺服器。 - Shared: 以 Common 類載入器為父類,是所有 Web 應用的父類載入器,其路徑由 `shared.loader` 指定,預設為空,此時 Tomcat 使用 Common 類載入器作為 Web 應用的父載入器。 - Web 應用: 以 Shared 類載入器為父類,載入 `/WEB-INF/classes` 目錄下的未壓縮的 Class 和資原始檔以及 `/WEB-INF/lib` 目錄下的 jar 包,該類載入器只對當前 Web 應用可見,對其他 Web 應用均不可見。 ## 4. Tomcat 類載入機制原始碼 ### 4.1 ClassLoader 的建立 先看下載入器類圖: ![](https://cdn.geekdigging.com/java/tomcat/tomcat6/WebappClassLoaderBase.png) 先從 BootStrap 的 main 方法看起: ```java public static void main(String args[]) { synchronized (daemonLock) { if (daemon == null) { // Don't set daemon until init() has completed Bootstrap bootstrap = new Bootstrap(); try { bootstrap.init(); } catch (Throwable t) { handleThrowable(t); t.printStackTrace(); return; } daemon = bootstrap; } else { // When running as a service the call to stop will be on a new // thread so make sure the correct class loader is used to // prevent a range of class not found exceptions. Thread.currentThread().setContextClassLoader(daemon.catalinaLoader); } // 省略其餘程式碼... } } ``` 可以看到這裡先判斷了 bootstrap 是否為 null ,如果不為 null 直接把 Catalina ClassLoader 設定到了當前執行緒,如果為 null 下面是走到了 init() 方法。 ```java public void init() throws Exception { // 初始化類載入器 initClassLoaders(); // 設定執行緒類載入器,將容器的載入器傳入 Thread.currentThread().setContextClassLoader(catalinaLoader); // 設定區安全類載入器 SecurityClassLoad.securityClassLoad(catalinaLoader); // 省略其餘程式碼... } ``` 接著這裡看到了會呼叫 `initClassLoaders()` 方法進行類載入器的初始化,初始化完成後,同樣會設定 Catalina ClassLoader 到當前執行緒。 ```java private void initClassLoaders() { try { commonLoader = createClassLoader("common", null); if (commonLoader == null) { // no config file, default to this loader - we might be in a 'single' env. commonLoader = this.getClass().getClassLoader(); } catalinaLoader = createClassLoader("server", commonLoader); sharedLoader = createClassLoader("shared", commonLoader); } catch (Throwable t) { handleThrowable(t); log.error("Class loader creation threw exception", t); System.exit(1); } } ``` 看到這裡應該就清楚了,會建立三個 ClassLoader : CommClassLoader , Catalina ClassLoader , SharedClassLoader ,正好對應前面介紹的三個基礎類載入器。 接著進入 `createClassLoader()` 檢視程式碼: ```java private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception { String value = CatalinaProperties.getProperty(name + ".loader"); if ((value == null) || (value.equals(""))) return parent; value = replace(value); List repositories = new ArrayList<>(); String[] repositoryPaths = getPaths(value); for (String repository : repositoryPaths) { // Check for a JAR URL repository try { @SuppressWarnings("unused") URL url = new URL(repository); repositories.add(new Repository(repository, RepositoryType.URL)); continue; } catch (MalformedURLException e) { // Ignore } // Local repository if (repository.endsWith("*.jar")) { repository = repository.substring (0, repository.length() - "*.jar".length()); repositories.add(new Repository(repository, RepositoryType.GLOB)); } else if (repository.endsWith(".jar")) { repositories.add(new Repository(repository, RepositoryType.JAR)); } else { repositories.add(new Repository(repository, RepositoryType.DIR)); } } return ClassLoaderFactory.createClassLoader(repositories, parent); } ``` 可以看到,這裡載入的資源正好是我們剛才看到的配置檔案 `conf/catalina.properties` 中的 `common.loader` , `server.loader` 和 `shared.loader` 。 ### 4.2 ClassLoader 載入過程 直接開啟 ParallelWebappClassLoader ,至於為啥不是看 WebappClassLoader ,從名字上就知道 ParallelWebappClassLoader 是一個並行的 WebappClassLoader 。 然後看下 ParallelWebappClassLoader 的 loadclass 方法是在它的父類 WebappClassLoaderBase 中實現的。 #### 4.2.1 第一步: ```java public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { if (log.isDebugEnabled()) log.debug("loadClass(" + name + ", " + resolve + ")"); Class clazz = null; // Log access to stopped class loader checkStateForClassLoading(name); // (0) Check our previously loaded local class cache clazz = findLoadedClass0(name); if (clazz != null) { if (log.isDebugEnabled()) log.debug(" Returning class from cache"); if (resolve) resolveClass(clazz); return clazz; } // 省略其餘... ``` 首先呼叫 `findLoaderClass0()` 方法檢查 WebappClassLoader 中是否載入過此類。 ```java protected Class findLoadedClass0(String name) { String path = binaryNameToPath(name, true); ResourceEntry entry = resourceEntries.get(path); if (entry != null) { return entry.loadedClass; } return null; } ``` WebappClassLoader 載入過的類都存放在 resourceEntries 快取中。 ```java protected final Map resourceEntries = new ConcurrentHashMap<>(); ``` #### 4.2.2 第二步: ```java // 省略其餘... clazz = findLoadedClass(name); if (clazz != null) { if (log.isDebugEnabled()) log.debug(" Returning class from cache"); if (resolve) resolveClass(clazz); return clazz; } // 省略其餘... ``` 如果第一步沒有找到,則繼續檢查 JVM 虛擬機器中是否載入過該類。呼叫 ClassLoader 的 `findLoadedClass()` 方法檢查。 #### 4.2.3 第三步: ```java ClassLoader javaseLoader = getJavaseClassLoader(); boolean tryLoadingFromJavaseLoader; try { URL url; if (securityManager != null) { PrivilegedAction dp = new PrivilegedJavaseGetResource(resourceName); url = AccessController.doPrivileged(dp); } else { url = javaseLoader.getResource(resourceName); } tryLoadingFromJavaseLoader = (url != null); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); tryLoadingFromJavaseLoader = true; } if (tryLoadingFromJavaseLoader) { try { clazz = javaseLoader.loadClass(name); if (clazz != null) { if (resolve) resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { // Ignore } } ``` 如果前兩步都沒有找到,則使用系統類載入該類(也就是當前 JVM 的 ClassPath )。為了防止覆蓋基礎類實現,這裡會判斷 class 是不是 JVMSE 中的基礎類庫中類。 #### 4.2.4 第四步: ```java boolean delegateLoad = delegate || filter(name, true); // (1) Delegate to our parent if requested if (delegateLoad) { if (log.isDebugEnabled()) log.debug(" Delegating to parent classloader1 " + parent); try { clazz = Class.forName(name, false, parent); if (clazz != null) { if (log.isDebugEnabled()) log.debug(" Loading class from parent"); if (resolve) resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { // Ignore } } ``` 先判斷是否設定了 delegate 屬性,設定為 true ,那麼就會完全按照 JVM 的"雙親委託"機制流程載入類。 若是預設的話,是先使用 WebappClassLoader 自己處理載入類的。當然,若是委託了,使用雙親委託亦沒有載入到 class 例項,那還是最後使用 WebappClassLoader 載入。 #### 4.2.5 第五步: ```java if (log.isDebugEnabled()) log.debug(" Searching local repositories"); try { clazz = findClass(name); if (clazz != null) { if (log.isDebugEnabled()) log.debug(" Loading class from local repository"); if (resolve) resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { // Ignore } ``` 若是沒有委託,則預設會首次使用 WebappClassLoader 來載入類。通過自定義 `findClass()` 定義處理類載入規則。 `findClass()` 會去 `Web-INF/classes` 目錄下查詢類。 #### 4.2.6 第六步: ```java if (!delegateLoad) { if (log.isDebugEnabled()) log.debug(" Delegating to parent classloader at end: " + parent); try { clazz = Class.forName(name, false, parent); if (clazz != null) { if (log.isDebugEnabled()) log.debug(" Loading class from parent"); if (resolve) resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { // Ignore } } ``` 若是 WebappClassLoader 在 `/WEB-INF/classes` 、 `/WEB-INF/lib` 下還是查詢不到 class ,那麼無條件強制委託給 System 、 Common 類載入器去查詢該類。 #### 4.2.7 小結 Web 應用類載入器預設的載入順序是: 1. 先從快取中載入; 2. 如果沒有,則從 JVM 的 Bootstrap 類載入器載入; 3. 如果沒有,則從當前類載入器載入(按照 WEB-INF/classes 、 WEB-INF/lib 的順序); 4. 如果沒有,則從父類載入器載入,由於父類載入器採用預設的委派模式,所以載入順序是 AppClassLoader 、 Common 、 Shared 。 ## 參考 https://www.jianshu.com/p/69c4