Tomcat的類加載機制是違反了雙親委托原則的,對於一些未加載的非基礎類(Object,String等),各個web應用自己的類加載器(WebAppClassLoader)會優先加載,加載不到時再交給commonClassLoader走雙親委托。
對於JVM來說:
因此,按照這個過程可以想到,如果同樣在CLASSPATH指定的目錄中和自己工作目錄中存放相同的class,會優先加載CLASSPATH目錄中的文件。
1、既然 Tomcat 不遵循雙親委派機制,那么如果我自己定義一個惡意的HashMap,會不會有風險呢?(阿里的面試官問)
答: 顯然不會有風險,如果有,Tomcat都運行這么多年了,那群Tomcat大神能不改進嗎? tomcat不遵循雙親委派機制,只是自定義的classLoader順序不同,但頂層還是相同的,
還是要去頂層請求classloader.
2、我們思考一下:Tomcat是個web容器, 那么它要解決什么問題:
1. 一個web容器可能需要部署兩個應用程序,不同的應用程序可能會依賴同一個第三方類庫的不同版本,不能要求同一個類庫在同一個服務器只有一份,因此要保證每個應用程序的類庫都是獨立的,保證相互隔離。
2. 部署在同一個web容器中相同的類庫相同的版本可以共享。否則,如果服務器有10個應用程序,那么要有10份相同的類庫加載進虛擬機,這是扯淡的。
3. web容器也有自己依賴的類庫,不能於應用程序的類庫混淆。基於安全考慮,應該讓容器的類庫和程序的類庫隔離開來。
4. web容器要支持jsp的修改,我們知道,jsp 文件最終也是要編譯成class文件才能在虛擬機中運行,但程序運行后修改jsp已經是司空見慣的事情,否則要你何用? 所以,web容器需要支持 jsp 修改后不用重啟。
再看看我們的問題:Tomcat 如果使用默認的類加載機制行不行?
答案是不行的。為什么?我們看,第一個問題,如果使用默認的類加載器機制,那么是無法加載兩個相同類庫的不同版本的,默認的累加器是不管你是什么版本的,只在乎你的全限定類名,並且只有一份。第二個問題,默認的類加載器是能夠實現的,因為他的職責就是保證唯一性。第三個問題和第一個問題一樣。我們再看第四個問題,我們想我們要怎么實現jsp文件的熱修改(樓主起的名字),jsp 文件其實也就是class文件,那么如果修改了,但類名還是一樣,類加載器會直接取方法區中已經存在的,修改后的jsp是不會重新加載的。那么怎么辦呢?我們可以直接卸載掉這jsp文件的類加載器,所以你應該想到了,每個jsp文件對應一個唯一的類加載器,當一個jsp文件修改了,就直接卸載這個jsp類加載器。重新創建類加載器,重新加載jsp文件。
Tomcat 如何實現自己獨特的類加載機制?
所以,Tomcat 是怎么實現的呢?牛逼的Tomcat團隊已經設計好了。我們看看他們的設計圖:
我們看到,前面3個類加載和默認的一致,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader則是Tomcat自己定義的類加載器,它們分別加載/common/*
、/server/*
、/shared/*
(在tomcat 6之后已經合並到根目錄下的lib目錄下)和/WebApp/WEB-INF/*
中的Java類庫。其中WebApp類加載器和Jsp類加載器通常會存在多個實例,每一個Web應用程序對應一個WebApp類加載器,每一個JSP文件對應一個Jsp類加載器。
- commonLoader:Tomcat最基本的類加載器,加載路徑中的class可以被Tomcat容器本身以及各個Webapp訪問;
- catalinaLoader:Tomcat容器私有的類加載器,加載路徑中的class對於Webapp不可見;
- sharedLoader:各個Webapp共享的類加載器,加載路徑中的class對於所有Webapp可見,但是對於Tomcat容器不可見;
- WebappClassLoader:各個Webapp私有的類加載器,加載路徑中的class只對當前Webapp可見;
從圖中的委派關系中可以看出:
CommonClassLoader能加載的類都可以被Catalina ClassLoader和SharedClassLoader使用,從而實現了公有類庫的共用,而CatalinaClassLoader和Shared ClassLoader自己能加載的類則與對方相互隔離。
WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個WebAppClassLoader實例之間相互隔離。
而JasperLoader的加載范圍僅僅是這個JSP文件所編譯出來的那一個.Class文件,它出現的目的就是為了被丟棄:當Web容器檢測到JSP文件被修改時,會替換掉目前的JasperLoader的實例,並通過再建立一個新的Jsp類加載器來實現JSP文件的HotSwap功能。
好了,至此,我們已經知道了tomcat為什么要這么設計,以及是如何設計的,那么,tomcat 違背了java 推薦的雙親委派模型了嗎?答案是:違背了。 我們前面說過:
雙親委派模型要求除了頂層的啟動類加載器之外,其余的類加載器都應當由自己的父類加載器加載。
很顯然,tomcat 不是這樣實現,tomcat 為了實現隔離性,沒有遵守這個約定,每個webappClassLoader加載自己的目錄下的class文件,不會傳遞給父類加載器。
我們擴展出一個問題:如果tomcat 的 Common ClassLoader 想加載 WebApp ClassLoader 中的類,該怎么辦?
看了前面的關於破壞雙親委派模型的內容,我們心里有數了,我們可以使用線程上下文類加載器實現,使用線程上下文加載器,可以讓父類加載器請求子類加載器去完成類加載的動作。牛逼吧。
類加載
在JVM中並不是一次性把所有的文件都加載到,而是一步一步的,按照需要來加載。
比如JVM啟動時,會通過不同的類加載器加載不同的類。當用戶在自己的代碼中,需要某些額外的類時,再通過加載機制加載到JVM中,並且存放一段時間,便於頻繁使用。
因此使用哪種類加載器、在什么位置加載類都是JVM中重要的知識。
JVM類加載
JVM類加載采用 父類委托機制,如下圖所示:
JVM中包括集中類加載器:
1 BootStrapClassLoader 引導類加載器
2 ExtClassLoader 擴展類加載器
3 AppClassLoader 應用類加載器
4 CustomClassLoader 用戶自定義類加載器
他們的區別上面也都有說明。需要注意的是,不同的類加載器加載的類是不同的,因此如果用戶加載器1加載的某個類,其他用戶並不能夠使用。
當JVM運行過程中,用戶需要加載某些類時,會按照下面的步驟(父類委托機制):
1 用戶自己的類加載器,把加載請求傳給父加載器,父加載器再傳給其父加載器,一直到加載器樹的頂層。
2 最頂層的類加載器首先針對其特定的位置加載,如果加載不到就轉交給子類。
3 如果一直到底層的類加載都沒有加載到,那么就會拋出異常ClassNotFoundException。
因此,按照這個過程可以想到,如果同樣在CLASSPATH指定的目錄中和自己工作目錄中存放相同的class,會優先加載CLASSPATH目錄中的文件。
Tomcat類加載
在tomcat中類的加載稍有不同,如下圖:
當tomcat啟動時,會創建幾種類加載器:
1 Bootstrap 引導類加載器
加載JVM啟動所需的類,以及標准擴展類(位於jre/lib/ext下)
2 System 系統類加載器
加載tomcat啟動的類,比如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定。位於CATALINA_HOME/bin下。
3 Common 通用類加載器
加載tomcat使用以及應用通用的一些類,位於CATALINA_HOME/lib下,比如servlet-api.jar
4 webapp 應用類加載器
每個應用在部署后,都會創建一個唯一的類加載器。該類加載器會加載位於 WEB-INF/lib下的jar文件中的class 和 WEB-INF/classes下的class文件。
當應用需要到某個類時,則會按照下面的順序進行類加載:
1 使用bootstrap引導類加載器加載
2 使用system系統類加載器加載
3 使用應用類加載器在WEB-INF/classes中加載
4 使用應用類加載器在WEB-INF/lib中加載
5 使用common類加載器在CATALINA_HOME/lib中加載
問題擴展
通過對上面tomcat類加載機制的理解,就不難明白 為什么java文件放在Eclipse中的src文件夾下會優先jar包中的class?
這是因為Eclipse中的src文件夾中的文件java以及webContent中的JSP都會在tomcat啟動時,被編譯成class文件放在 WEB-INF/class 中。
而Eclipse外部引用的jar包,則相當於放在 WEB-INF/lib 中。
因此肯定是 java文件或者JSP文件編譯出的class優先加載。
通過這樣,我們就可以簡單的把java文件放置在src文件夾中,通過對該java文件的修改以及調試,便於學習擁有源碼java文件、卻沒有打包成xxx-source的jar包。
另外呢,開發者也會因為粗心而犯下面的錯誤。
在 CATALINA_HOME/lib 以及 WEB-INF/lib 中放置了 不同版本的jar包,此時就會導致某些情況下報加載不到類的錯誤。
還有如果多個應用使用同一jar包文件,當放置了多份,就可能導致 多個應用間 出現類加載不到的錯誤。
1. java類加載器
近來了解tomcat的類加載機制,所以先回顧一下java虛擬機類加載器,如果從java虛擬機的角度來看的話,其實類加載器只分為兩種:一種是啟動類加載器(即Bootstrap ClassLoader),通過使用JNI來實現,我們無法獲取到到它的實例;另一種則是java語言實現java.lang.ClassLoader
的子類。一般從我們的角度來看,會根據類加載路徑會把類加載器分為3種:Bootstrap ClassLoader,ExtClassLoader,AppClassLoader.后兩者是sun.misc.Launcher
類的內部類,而前者在JDK源碼中是沒有與之對應的類的,倒是在sun.misc.Launcher
中可以看到一些它的加載路徑信息。如果找不到sun的源碼,可以下載OpenJDK的來看一下。
Bootstrap ClassLoader: 引導類加載器,從%JAVA_RUNTIME_JRE%/lib目錄加載,但並不是將該目錄所有的類庫都加載,它會加載一些符合文件名稱的,例如:rt.jar,resources.jar等。在sun.misc.Launcher
源碼中也可以看得它的加載路徑:
private static String bootClassPath = System.getProperty("sun.boot.class.path");
- 1
或者配置-Xbootclasspath參數指定加載的路徑,通過獲取環境變量sun.boot.class.path
看一下到底具體加載了那些類:
D:\Program Files\Java\jdk1.7.0_67\jre\lib\resources.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\rt.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\sunrsasign.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\jsse.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\jce.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\charsets.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\jfr.jar
D:\Program Files\Java\jdk1.7.0_67\jre\classes
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
Extension ClassLoader:擴展類加載器,實現類為sun.misc.Launcher$ExtClassLoader
,加載%JAVA_RUNTIME_JRE%/lib/ext/目錄下的jar包,也可以在sun.misc.Launcher
源碼中也可以看得它的加載路徑:
String s = System.getProperty("java.ext.dirs");
- 1
通過獲取java.ext.dirs
環境變量打印一下:
D:\Program Files\Java\jdk1.7.0_67\jre\lib\ext
- 1
Appication ClassLoader:應用程序類加載器,或者叫系統類加載器,實現類為sun.misc.Launcher$AppClassLoader
。從sun.misc.Launcher
的構造函數中可以看到,當AppClassLoader
被初始化以后,它會被設置為當前線程的上下文類加載器以及保存到Launcher
類的loader屬性中,而通過ClassLoader.getSystemClassLoader()
獲取的也正是該類加載器(Launcher.loader)。應用類加載器從用戶類路徑中加載類庫,可以在源碼中看到:
final String s = System.getProperty("java.class.path");
- 1
1.1 類關系
由圖看到Bootstrap ClassLoader並不在繼承鏈上,因為它是java虛擬機內置的類加載器,對外不可見。可以看到頂層ClassLoader
有一個parent屬性,用來表示着類加載器之間的層次關系(雙親委派模型);注意,ExtClassLoader
類在初始化時顯式指定了parent為null,所以它的父類加載器默認為Bootstrap ClassLoader
。在tomcat中都是通過擴展URLClassLoader
來實現自己的類加載器。
1.2 雙親委托模型
這3種類加載器之間存在着父子關系(區別於java里的繼承),子加載器保存着父加載器的引用。當一個類加載器需要加載一個目標類時,會先委托父加載器去加載,然后父加載器會在自己的加載路徑中搜索目標類,父加載器在自己的加載范圍中找不到時,才會交還給子加載器加載目標類。
采用雙親委托模式可以避免類加載混亂,而且還將類分層次了,例如java中lang包下的類在jvm啟動時就被啟動類加載器加載了,而用戶一些代碼類則由應用程序類加載器(AppClassLoader)加載,基於雙親委托模式,就算用戶定義了與lang包中一樣的類,最終還是由應用程序類加載器委托給啟動類加載器去加載,這個時候啟動類加載器發現已經加載過了lang包下的類了,所以兩者都不會再重新加載。當然,如果使用者通過自定義的類加載器可以強行打破這種雙親委托模型,但也不會成功的,java安全管理器拋出將會拋出java.lang.SecurityException
異常。
- 啟動類加載器是擴展類加載器的父類加載器:擴展類加載器在
sun.misc.Launcher
構造函數中被初始化,它的父類加載器被設置了為null,那為什么還說啟動類加載器是它的父加載器?看一下ClassLoader.loadClass()
方法:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先,查找該類是否已經被加載過了 Class c = findLoadedClass(name); if (c == null) { //未被加載過 long t0 = System.nanoTime(); try { if (parent != null) { // 父類加載器不為null,則調用父類加載器嘗試加載 c = parent.loadClass(name, false); } else { // 父類加載器為null,則調用本地方法,交由啟動類加載器加載,所以說ExtClassLoader的父類加載器為Bootstrap ClassLoader c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null) { //仍然加載不到,只能由本加載器通過findClass去加載 long t1 = System.nanoTime(); 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; } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
從代碼中看到,如果parent==null,將會由啟動類加載器嘗試加載,所以擴展類加載器的父類加載器是啟動類加載器。
- 擴展類加載器是應用程序類加載器的父類加載器:這個比較好理解,依然是在
sun.misc.Launcher
構造函數初始化應用程序類加載器時,指定了ExtClassLoader為AppClassLoader的父類加載器:
loader = AppClassLoader.getAppClassLoader(extcl);//loader是ClassLoader的屬性,extcl是擴展類加載器實例
- 1
- 應用程序類加載器是自定義類加載器的父類加載器:這里指的是使用默認構造函數進行自定義類加載器(否則 你可以指定parent來構造一個父加載器為ExtClassLoader的自定義類加載器),無論是通過擴展ClassLoader還是URLClassLoader最終都會獲取系統類加載器(AppClassLoader)作為父類加載器:
protected ClassLoader() { //調用getSystemClassLoader方法獲取系統類加載器作為父類加載器 this(checkCreateClassLoader(), getSystemClassLoader()); } public static ClassLoader getSystemClassLoader() { initSystemClassLoader(); //初始化系統類加載器 ..... return scl; } private static synchronized void initSystemClassLoader() { ...... sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); ...... scl = l.getClassLoader(); //這里拿到的就是在Launcher構造函數中構造的AppClassLoader實例 ...... } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
2. tomcat7類加載器
tomcat作為一個java web容器,也有自己的類加載機制,通過自定義的類加載機制以實現共享類庫的抽取,不同web應用之間的資源隔離還有熱加載等功能。除了一些java自身的一些類加載器處,它實現的主要類加載器有:Common ClassLoader,Catalina ClassLoader,Shared ClassLoader以及WebApp ClassLoader.通過下面類關系圖以及邏輯關系圖,同時對比上文內容梳理這些類加載器之間的關系。
2.1 類關系圖
從圖中看到了Common,Catalina,Shared類加載器是URLClassLoader類的一個實例,只是它們的類加載路徑不一樣,在tomcat/conf/catalina.properties配置文件中配置(common.loader,server.loader,shared.loader).WebAppClassLoader繼承自WebAppClassLoaderBase,基本所有邏輯都在WebAppClassLoaderBase為中實現了,可以看出tomcat的所有類加載器都是以URLClassLoader為基礎進行擴展。
2.2 邏輯關系圖
上面說到Common,Catalina,Shared類加載器是URLClassLoader類的一個實例,在默認的配置中,它們其實都是同一個對象,即commonLoader,結合初始化時的代碼(只保留關鍵代碼):
private void initClassLoaders() { commonLoader = createClassLoader("common", null); // commonLoader的加載路徑為common.loader if( commonLoader == null ) { commonLoader=this.getClass().getClassLoader(); } catalinaLoader = createClassLoader("server", commonLoader); // 加載路徑為server.loader,默認為空,父類加載器為commonLoader sharedLoader = createClassLoader("shared", commonLoader); // 加載路徑為shared.loader,默認為空,父類加載器為commonLoader } private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception { String value = CatalinaProperties.getProperty(name + ".loader"); if ((value == null) || (value.equals(""))) return parent; // catalinaLoader與sharedLoader的加載路徑均為空,所以直接返回commonLoader對象,默認3者為同一個對象 }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
在上面的代碼初始化時很明確是指出了,catalina與shared類加載器的父類加載器為common類加載器,而初始化commonClassLoader時父類加載器設置為null,最終會調到createClassLoader
靜態方法:
public static ClassLoader createClassLoader(List<Repository> repositories, final ClassLoader parent) throws Exception { ..... return AccessController.doPrivileged( new PrivilegedAction<URLClassLoader>() { @Override public URLClassLoader run() { if (parent == null) return new URLClassLoader(array); //該構造方法默認獲取系統類加載器為父類加載器,即AppClassLoader else return new URLClassLoader(array, parent); } }); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
在createClassLoader
中指定參數parent==null
時,最終會以系統類加載器(AppClassLoader)作為父類加載器,這解釋了為什么commonClassLoader的父類加載器是AppClassLoader.
一個web應用對應着一個StandardContext
實例,每個web應用都擁有獨立web應用類加載器(WebClassLoader),這個類加載器在StandardContext.startInternal()
中被構造了出來:
if (getLoader() == null) { WebappLoader webappLoader = new WebappLoader(getParentClassLoader()); webappLoader.setDelegate(getDelegate()); setLoader(webappLoader); }
- 1
- 2
- 3
- 4
- 5
這里getParentClassLoader()
會獲取父容器StandarHost.parentClassLoader
對象屬性,而這個對象屬性是在Catalina$SetParentClassLoaderRule.begin()
初始化,初始化的值其實就是Catalina.parentClassLoader
對象屬性,再來跟蹤一下Catalina.parentClassLoader
,在Bootstrap.init()
時通過反射調用了Catalina.setParentClassLoader()
,將Bootstrap.sharedLoader
屬性設置為Catalina.parentClassLoader
,所以WebClassLoader的父類加載器是Shared ClassLoader.
2.3 類加載邏輯
tomcat的類加載機制是違反了雙親委托原則的,對於一些未加載的非基礎類(Object,String等),各個web應用自己的類加載器(WebAppClassLoader)會優先加載,加載不到時再交給commonClassLoader走雙親委托。具體的加載邏輯位於WebAppClassLoaderBase.loadClass()
方法中,代碼篇幅長,這里以文字描述加載一個類過程:
- 先在本地緩存中查找是否已經加載過該類(對於一些已經加載了的類,會被緩存在
resourceEntries
這個數據結構中),如果已經加載即返回,否則 繼續下一步。 - 讓系統類加載器(AppClassLoader)嘗試加載該類,主要是為了防止一些基礎類會被web中的類覆蓋,如果加載到即返回,返回繼續。
- 前兩步均沒加載到目標類,那么web應用的類加載器將自行加載,如果加載到則返回,否則繼續下一步。
- 最后還是加載不到的話,則委托父類加載器(Common ClassLoader)去加載。
第3第4兩個步驟的順序已經違反了雙親委托機制,除了tomcat之外,JDBC,JNDI,Thread.currentThread().setContextClassLoader();
等很多地方都一樣是違反了雙親委托。