目錄
一、JVM 類加載機制
二、Tomcat 類加載器
? ? ? ? 2.2 findClass 介紹
? ? ? ? 3.2 loadClass 介紹
三、web應用隔離
? ? ? ? 3.1 Spring 加載問題
? ? ? ? 在開始文章內容之前,先來看三個問題
- 假如在 Tomcat 上運行了兩個 Web 應用程序,兩個 web 應用中有同名的Servlet,比如都叫UserController,但是功能不同,Tomcat 需要同時加載和管理這兩個同名的 Servlet 類,保證他們不會沖突,那怎么才能實現隔離?
- 假如兩個 web 應用都依賴同一個第三方 jar 包,比如spring,那 spring 的 jar 包被加載到內存后,Tomcat 保證這兩個 web 應用能共享,也就是說 spring 的 jar 包只被加載一次,否則隨著依賴第三方的增多,JVM的內存會爆炸,這時怎么做到的?
- 跟JVM一樣,怎樣隔離 Tomcat 本身和 web 應用類?
? ? ? ? 以上三個問題本文會逐一來講解,下面先來看下 JVM 的類加載機制。
一、JVM 類加載機制
????????Java 的類加載就是把字節碼格式 “.class” 文件加載到 JVM 方法區,并在 JVM 的堆中建立一個 java.lang.class 對象實例,用來封裝 Java 類相關的數據和方法。
????????JVM 并不是在啟動時就把所有的 “.class” 文件都加載一遍,而是程序在運行過程中用到了這個類才去加載。JVM 類加載是由類加載器來完成的,JDK 提供一個抽象類 ClassLoader,這個抽象類中定義了三個關鍵方法理解清楚他們的作用和關系非常重要。
- JVM 的類加載器是有層次結構的,他們有父子關系,每個類加載器都有一個 parent 字段,指向父類加載器
- defineClass 是個工具方法,它的職責是調用 native 方法把 Java 類的字節碼解析成一個Class 對象,所謂的 native 方法就是由C語言實現的方法,Java通過 JNI 機制調用
- findClass 方法的主要職責就是找到 “.class” 文件,可能來自文件系統或者網絡,找到后把“.class” 文件讀到內存得到字節碼,然后調用 defineClass 方法獲得 Class 對象
- loadClass 是個 public 方法,說明它才是對外提供服務的接口,具體實現也比較清晰:首先檢查這個類是不是已經被加載過了,如果加載過了直接返回,否則交給父加載器去加載。注意,這是一個遞歸調用,也就是說子加載器持有父加載器的引用,當一個類加載器需要加載一個 Java 類時,會先委托父加載器去加載,然后父加載器在自己的加載路徑中搜索 Java 類,當父加載器在自己的加載范圍內找不到時,才會交還給子加載器加載,這就是雙親委托機制。
public abstract class ClassLoader {// 每個類加載器都有個父加載器private final ClassLoader parent;public Class<?> loadClass(String name) {// 查找一下這個類是不是已經加載過了Class<?> c = findLoadedClass(name);// 如果沒有加載過if( c == null ){// 先委托給父加載器去加載,注意這是個遞歸調用if (parent != null) {c = parent.loadClass(name);} else {// 如果父加載器為空,查找Bootstrap加載器是不是加載過了c = findBootstrapClassOrNull(name);}}// 如果父加載器沒加載成功,調用自己的findClass去加載if (c == null) {c = findClass(name);}return c;}protected Class<?> findClass(String name){//1. 根據傳入的類名name,到在特定目錄下去尋找類文件,把.class文件讀入內存...//2. 調用defineClass將字節數組轉成Class對象return defineClass(buf, off, len);}// 將字節碼數組解析成一個Class對象,用native方法實現protected final Class<?> defineClass(byte[] b, int off, int len){...}
}
? ? ? ? JVM 雙親委派如圖
- BootstrapClassLoader 是啟動類加載器,由 C 語言實現,用來加載 JVM 啟動時所需要的核心類,比如 rt.jar、resource.jar 等。
- ExtClassLoader 是擴展類加載器,用來接在 \jre\lib\ext 目錄下的 jar 包。
- AppClassLoader 是系統類加載器,用來加載 classpath 下的類,應用程序默認用它來加載類。
- 自定義類加載器,用來加載自定義路徑下的類。
????????這些類加載器的工作原理是一樣的,區別是他們的加載路徑不同,也就是說 findClass 這個方法查找的路徑不同。雙親委派機制是為了保證一個 Java 類在 JVM 中是唯一的,假如不小心寫了一個與 JRE 核心類同名的類,比如 Object 類,雙親委派機制能保證加載的是 JRE 里的那個Object 類,而不是你自己寫的 Object 類。這是因為 AppClassLoader 在加載你的 Object 類時,會委托給 ExtClassLoader 去加載,而 ExtClassLoader 又會委托給 BootstrapClassLoader,BootstrapClassLoader 發現自己已經加載過了 Object 類,會直接返回,不會去加載你寫的 Object 類。
????????注意,類加載器的父子關系不是通過繼承來實現的,比如 AppClassLoader 并不是 ExtClassLoader 的子類,而是說 AppClassLoader 的 parent 成員變量指向 ExtClassLoader 對象。
二、Tomcat 類加載器
????????Tomca t的自定義類加載器 WebAppClassLoader 打破了雙親委派機制,首先自己嘗試去加載某個類,如果找不到再代理給父類加載器,其目的是優先加載 Web 應用自己定義的類。具體實現就是重寫 ClassLoader 的兩個方法:findClass 和 loadClass。看下下面的源碼
? ? ? ? 2.2 findClass 介紹
public Class<?> findClass(String name) throws ClassNotFoundException {...Class<?> clazz = null;try {//1. 先在Web應用目錄下查找類 clazz = findClassInternal(name);} catch (RuntimeException e) {throw e;}if (clazz == null) {try {//2. 如果在本地目錄沒有找到,交給父加載器去查找clazz = super.findClass(name);} catch (RuntimeException e) {throw e;}//3. 如果父類也沒找到,拋出ClassNotFoundExceptionif (clazz == null) {throw new ClassNotFoundException(name);}return clazz;
}
?在 findClass 方法里,主要有三個步驟:
- 先在 Web 應用本地目錄下查找要加載的類。
- 如果沒有找到,交給父加載器去查找,它的父加載器就是上面提到的系統類加載器 AppClassLoader。
- 如果父加載器也沒找到這個類,拋出 ClassNotFound 異常。
? ? ? ? 3.2 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);
}
????????loadClass 方法稍微復雜一點,主要有六個步驟:
- 先在本地 Cache 查找該類是否已經加載過,也就是說 Tomcat 的類加載器是否已經加載過這個類。
- 如果 Tomcat 類加載器沒有加載過這個類,再看看系統類加載器是否加載過。
- 如果都沒有,就讓 ExtClassLoader 去加載,這一步比較關鍵,目的防止 Web 應用自己的類覆蓋 JRE 的核心類。因為 Tomcat 需要打破雙親委派機制,假如 Web 應用里自定義了一個叫 Object 的類,如果先加載這個 Object 類,就會覆蓋 JRE 里面的那個 Object 類,這就是為什么 Tomcat 的類加載器會優先嘗試用 ExtClassLoader 去加載,因為 ExtClassLoader 會委托給 BootstrapClassLoader 去加載,BootstrapClassLoader 發現自己已經加載了 Object 類,直接返回給 Tomcat 的類加載器,這樣 Tomcat 的類加載器就不會去加載 Web 應用下的 Object 類了,也就避免了覆蓋 JRE 核心類的問題。
- 如果 ExtClassLoader 加載器加載失敗,也就是說 JRE 核心類中沒有這類,那么就在本地 Web 應用目錄下查找并加載。
- 如果本地目錄下沒有這個類,說明不是 Web 應用自己定義的類,那么由系統類加載器去加載。這里請你注意,Web 應用是通過Class.forName調用交給系統類加載器的,因為Class.forName的默認加載器就是系統類加載器。
- 如果上述加載過程全部失敗,拋出 ClassNotFound 異常。
????????Tomcat 的類加載器打破了雙親委派機制,沒有一上來就直接委托給父加載器,而是先在本地目錄下加載,為了避免本地目錄下的類覆蓋 JRE 的核心類,先嘗試用 JVM 擴展類加載器 ExtClassLoader 去加載。
三、web應用隔離
? ? ? ? 先回答開頭提到的第一個問題,如果我們使用 JVM 的 AppClassLoader 來加載 web 應用,AppClassLoader 只能加載一個 Servlet,再加載第二個同名的 Servlet 時,會返回第一個加載的 Servlet,同名的只被加載一次。Tomcat的解決方案是自定義一個類加載器 WebAppClassLoader,并且給每個 web 應用創建一個類加載器。不同的類加載器加載的類被認為是不同的類,即使名稱相同,web 應用通過各自的類加載器來實現隔離。
? ? ? ? 在來看第二個問題,多個 web 應用之間需要共享類庫,并且不能重復加載相同的類。在雙親委派機制里,各個子加載器都可以通過父加載器去加載類,那么把需要共享的類放到父加載器的加載路徑下不就行了嗎,應用程序也是通過這種方式共享 JRE 核心類。因此 Tomcat 的設計者又加了一個類加載器 SharedClassLoader,作為 WebAppClassLoader 的父加載器,專門來加載web 應用之間共享的類。如果 WebAppClassLoader 自己沒有加載到某個類,就會委托父加載器SharedClassLoader 去加載這個類,SharedClassLoader 會在指定目錄下加載共享類,之后返回給 WebAppClassLoader,這樣共享的問題就結局了。
? ? ? ? 第三個問題,如何隔離 Tomcat 本身的類和 web 應用的類?要共享可以通過父子關系,要隔離那就需要兄弟關系了。兄弟關系就是指兩個類加載器是平行的,他們可能擁有同一個父加載器,但是兩個兄弟類加載器加載的類是隔離的。因此 Tomcat 又設計了一個類加載器CatalinaClassLoader,專門來加載 Tomcat 自身的類,這樣設計有個問題,那 Tomcat 和 web 需要共享一些類時怎么辦呢?
????????還是再增加一個 CommonClassLoader,作為 CatalinaClassLoader 和 SharedClassLoader 的父加載器。CommonClassLoader 能加載的類都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 能加載的類則與對方相互隔離。WebAppClassLoader 可以使用 SharedClassLoader 加載到的類,但各個 WebAppClassLoader 實例之間相互隔離。
? ? ? ? 3.1 Spring 加載問題
? ? ? ? 在 JVM 的實現中有一條規則,如果一個類由類加載器 A 加載,那么這個類的依賴類也由相同的類加載器完成。Spring 作為一個 bean 工廠,需要創建業務實體類,并且在創業業務類之前只要創建依賴類。
? ? ? ? 前面提到,web 應用之間共享的 JAR 包可以交給 SharedClassLoader 來加載,從而避免了重復,Spring 作為共享的三方 JAR 包,它自己是由?SharedClassLoader 加載的,但是Spring又要去加載業務類,但是業務類不在?SharedClassLoader 對應的目錄下,那該怎么辦呢?
? ? ? ? Tomcat 使用了線程上下文加載器,它其實是一種類加載傳遞機制。這個類加載器保存在線程私有數據里,只要是同一個線程,一旦設置了線程上下文加載器,在線程后續執行過程中,就能把這個加載器取出來用。
????????Tomcat 為每個 Web 應用創建一個 WebAppClassLoader 類加載器,并在啟動 Web 應用的線程里設置線程上下文加載器,這樣 Spring 在啟動時就將線程上下文加載器取出來,用來加載 Bean。這樣就完成了?SharedClassLoader 創建的 Spring 可以創建?WebAppClassLoader 下的業務類,是不是設計的很精妙呢?
? ? ? ? 好了本期內容就介紹到這里。
往期經典推薦:
Tomcat架構究竟是什么?靈魂原來在這里-CSDN博客
你真的了解Tomcat一鍵啟停嗎?-CSDN博客
你所不知的Tomcat網絡通信的玄機-CSDN博客
決勝高并發戰場:Redis并發訪問控制與實戰解析-CSDN博客
TiDB內核解密:揭秘其底層KV存儲引擎如何玩轉鍵值對-CSDN博客
????????