淺談Tomcat如何打破雙親委托機制
我們經常會遇到ClassNotFound異常,表明JVM在嘗試加載某類時失敗瞭。
要解決這個異常,你得知道
- 什麼是類加載
- JVM如何加載類
- 為什麼會出現ClassNotFound
想想Tomcat又是如何加載和管理Web應用下的Servlet呢?
Tomcat正是通過Context組件來加載管理Web應用的,所以今天我會詳細分析Tomcat的類加載機制。但在這之前,我們有必要預習一下JVM的類加載機制,我會先回答一下一開始拋出來的問題,接著再談談Tomcat的類加載器如何打破Java的雙親委托機制。
JVM的類加載器
Java的類加載,就是把字節碼格式.class文件加載到JVM的方法區,並在JVM堆建立一個java.lang.Class對象實例,封裝Java類相關的數據和方法。
Class對象是什麼?
可以理解成業務類的模板,JVM根據該模板創建具體業務類對象實例。
JVM並非在啟動時就把所有 .class 文件都加載一遍,而是程序在運行過程中用到該類才去加載。
JVM類加載由類加載器完成,JDK提供一個抽象類ClassLoader:
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的類加載器是分層的父子關系,每個類加載器都持有一個parent字段指向父加載器。
- defineClass 工具方法:調用native方法把Java類的字節碼解析成一個Class對象
- findClass 就是找到 .class 文件,可能來自文件系統或網絡,找到後把 .class 文件讀到內存得到字節碼數組,然後調用defineClass方法得到Class對象
loadClass 首先檢查這個類是不是已經被加載過瞭,如果加載過瞭直接返回,否則交給父加載器去加載。
這是個遞歸調用,即子加載器持有父加載器引用,當一個類加載器需加載一個Java類時,會先委托父加載器去加載,然後父加載器在自己加載路徑中搜索Java類,當父加載器在自己的加載范圍內找不到時,才會交還給子加載器加載,這就是雙親委托機制。
JDK的類加載器工作原理是一樣的,區別隻是加載路徑不同,即findClass查找的路徑不同。
雙親委托機制是為保證一個Java類在JVM的唯一性。假如你手滑寫個與JRE核心類同名類,比如Object,雙親委托機制能保證加載的是JRE裡的那個Object類,而不是你寫的Object。
因為AppClassLoader在加載你的Object類時,會委托給ExtClassLoader去加載,而ExtClassLoader又會委托給BootstrapClassLoader,BootstrapClassLoader發現自己已經加載過瞭Object類,會直接返回,不會去加載你的Object類。
類加載器的父子關系不是通過繼承來實現的,比如AppClassLoader並非ExtClassLoader的子類,隻是AppClassLoader的parent指向ExtClassLoader對象。
所以若自定義類加載器,不是去繼承AppClassLoader,而是繼承ClassLoader抽象類,再重寫findClass和loadClass即可。
Tomcat就是通過自定義類加載器實現自己的類加載。
若你要打破雙親委托,也就隻需重寫loadClass,因為loadClass的默認實現就是雙親委托機制。
Tomcat的類加載器
Tomcat的自定義類加載器WebAppClassLoader打破瞭雙親委托機制:
首先自己嘗試去加載某個類,如果找不到再委托給父類加載器,目的是優先加載Web應用自己定義的類。
隻需重寫ClassLoader的兩個方法:
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. 如果父類也沒找到,拋出ClassNotFoundException if (clazz == null) { throw new ClassNotFoundException(name); } return clazz; }
工作流程
- 先在Web應用本地目錄下查找要加載的類
- 若未找到,交給父加載器查找,即AppClassLoader
- 若父加載器也沒找到這個類,拋ClassNotFound
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類加載器尚未加載過該類,再看看系統類加載器是否加載過
- 若都沒有,就讓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核心類,會先嘗試用ExtClassLoader加載。
那為何不先用AppClassLoader加載?
若這樣,就又變成雙親委托,這就是Tomcat類加載器的奧妙。
到此這篇關於淺談Tomcat如何打破雙親委托機制的文章就介紹到這瞭,更多相關Tomcat 雙親委托機制內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!