图解Tomcat类加载机制(阿里面试题)
Tomcat的類加載機(jī)制是違反了雙親委托原則的,對(duì)于一些未加載的非基礎(chǔ)類(Object,String等),各個(gè)web應(yīng)用自己的類加載器(WebAppClassLoader)會(huì)優(yōu)先加載,加載不到時(shí)再交給commonClassLoader走雙親委托。?
對(duì)于JVM來說:
因此,按照這個(gè)過程可以想到,如果同樣在CLASSPATH指定的目錄中和自己工作目錄中存放相同的class,會(huì)優(yōu)先加載CLASSPATH目錄中的文件。
?
?
?1、既然 Tomcat 不遵循雙親委派機(jī)制,那么如果我自己定義一個(gè)惡意的HashMap,會(huì)不會(huì)有風(fēng)險(xiǎn)呢?(阿里的面試官問)
答: 顯然不會(huì)有風(fēng)險(xiǎn),如果有,Tomcat都運(yùn)行這么多年了,那群Tomcat大神能不改進(jìn)嗎? tomcat不遵循雙親委派機(jī)制,只是自定義的classLoader順序不同,但頂層還是相同的,
還是要去頂層請(qǐng)求classloader.
2、我們思考一下:Tomcat是個(gè)web容器, 那么它要解決什么問題:?
1. 一個(gè)web容器可能需要部署兩個(gè)應(yīng)用程序,不同的應(yīng)用程序可能會(huì)依賴同一個(gè)第三方類庫的不同版本,不能要求同一個(gè)類庫在同一個(gè)服務(wù)器只有一份,因此要保證每個(gè)應(yīng)用程序的類庫都是獨(dú)立的,保證相互隔離。?
2. 部署在同一個(gè)web容器中相同的類庫相同的版本可以共享。否則,如果服務(wù)器有10個(gè)應(yīng)用程序,那么要有10份相同的類庫加載進(jìn)虛擬機(jī),這是扯淡的。?
3. web容器也有自己依賴的類庫,不能于應(yīng)用程序的類庫混淆。基于安全考慮,應(yīng)該讓容器的類庫和程序的類庫隔離開來。?
4. web容器要支持jsp的修改,我們知道,jsp 文件最終也是要編譯成class文件才能在虛擬機(jī)中運(yùn)行,但程序運(yùn)行后修改jsp已經(jīng)是司空見慣的事情,否則要你何用? 所以,web容器需要支持 jsp 修改后不用重啟。
再看看我們的問題:Tomcat 如果使用默認(rèn)的類加載機(jī)制行不行??
答案是不行的。為什么?我們看,第一個(gè)問題,如果使用默認(rèn)的類加載器機(jī)制,那么是無法加載兩個(gè)相同類庫的不同版本的,默認(rèn)的累加器是不管你是什么版本的,只在乎你的全限定類名,并且只有一份。第二個(gè)問題,默認(rèn)的類加載器是能夠?qū)崿F(xiàn)的,因?yàn)樗穆氊?zé)就是保證唯一性。第三個(gè)問題和第一個(gè)問題一樣。我們?cè)倏吹谒膫€(gè)問題,我們想我們要怎么實(shí)現(xiàn)jsp文件的熱修改(樓主起的名字),jsp 文件其實(shí)也就是class文件,那么如果修改了,但類名還是一樣,類加載器會(huì)直接取方法區(qū)中已經(jīng)存在的,修改后的jsp是不會(huì)重新加載的。那么怎么辦呢?我們可以直接卸載掉這jsp文件的類加載器,所以你應(yīng)該想到了,每個(gè)jsp文件對(duì)應(yīng)一個(gè)唯一的類加載器,當(dāng)一個(gè)jsp文件修改了,就直接卸載這個(gè)jsp類加載器。重新創(chuàng)建類加載器,重新加載jsp文件。
Tomcat 如何實(shí)現(xiàn)自己獨(dú)特的類加載機(jī)制?
所以,Tomcat 是怎么實(shí)現(xiàn)的呢?牛逼的Tomcat團(tuán)隊(duì)已經(jīng)設(shè)計(jì)好了。我們看看他們的設(shè)計(jì)圖:
我們看到,前面3個(gè)類加載和默認(rèn)的一致,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader則是Tomcat自己定義的類加載器,它們分別加載/common/*、/server/*、/shared/*(在tomcat 6之后已經(jīng)合并到根目錄下的lib目錄下)和/WebApp/WEB-INF/*中的Java類庫。其中WebApp類加載器和Jsp類加載器通常會(huì)存在多個(gè)實(shí)例,每一個(gè)Web應(yīng)用程序?qū)?yīng)一個(gè)WebApp類加載器,每一個(gè)JSP文件對(duì)應(yīng)一個(gè)Jsp類加載器。
- commonLoader:Tomcat最基本的類加載器,加載路徑中的class可以被Tomcat容器本身以及各個(gè)Webapp訪問;
- catalinaLoader:Tomcat容器私有的類加載器,加載路徑中的class對(duì)于Webapp不可見;
- sharedLoader:各個(gè)Webapp共享的類加載器,加載路徑中的class對(duì)于所有Webapp可見,但是對(duì)于Tomcat容器不可見;
- WebappClassLoader:各個(gè)Webapp私有的類加載器,加載路徑中的class只對(duì)當(dāng)前Webapp可見;
從圖中的委派關(guān)系中可以看出:
CommonClassLoader能加載的類都可以被Catalina ClassLoader和SharedClassLoader使用,從而實(shí)現(xiàn)了公有類庫的共用,而CatalinaClassLoader和Shared ClassLoader自己能加載的類則與對(duì)方相互隔離。
WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個(gè)WebAppClassLoader實(shí)例之間相互隔離。
而JasperLoader的加載范圍僅僅是這個(gè)JSP文件所編譯出來的那一個(gè).Class文件,它出現(xiàn)的目的就是為了被丟棄:當(dāng)Web容器檢測(cè)到JSP文件被修改時(shí),會(huì)替換掉目前的JasperLoader的實(shí)例,并通過再建立一個(gè)新的Jsp類加載器來實(shí)現(xiàn)JSP文件的HotSwap功能。
好了,至此,我們已經(jīng)知道了tomcat為什么要這么設(shè)計(jì),以及是如何設(shè)計(jì)的,那么,tomcat 違背了java 推薦的雙親委派模型了嗎?答案是:違背了。 我們前面說過:
雙親委派模型要求除了頂層的啟動(dòng)類加載器之外,其余的類加載器都應(yīng)當(dāng)由自己的父類加載器加載。
很顯然,tomcat 不是這樣實(shí)現(xiàn),tomcat 為了實(shí)現(xiàn)隔離性,沒有遵守這個(gè)約定,每個(gè)webappClassLoader加載自己的目錄下的class文件,不會(huì)傳遞給父類加載器。
我們擴(kuò)展出一個(gè)問題:如果tomcat 的 Common ClassLoader 想加載 WebApp ClassLoader 中的類,該怎么辦?
看了前面的關(guān)于破壞雙親委派模型的內(nèi)容,我們心里有數(shù)了,我們可以使用線程上下文類加載器實(shí)現(xiàn),使用線程上下文加載器,可以讓父類加載器請(qǐng)求子類加載器去完成類加載的動(dòng)作。牛逼吧。
?
?
類加載
在JVM中并不是一次性把所有的文件都加載到,而是一步一步的,按照需要來加載。
比如JVM啟動(dòng)時(shí),會(huì)通過不同的類加載器加載不同的類。當(dāng)用戶在自己的代碼中,需要某些額外的類時(shí),再通過加載機(jī)制加載到JVM中,并且存放一段時(shí)間,便于頻繁使用。
因此使用哪種類加載器、在什么位置加載類都是JVM中重要的知識(shí)。
JVM類加載
JVM類加載采用 父類委托機(jī)制,如下圖所示:
JVM中包括集中類加載器:
1 BootStrapClassLoader 引導(dǎo)類加載器
2 ExtClassLoader 擴(kuò)展類加載器
3 AppClassLoader 應(yīng)用類加載器
4 CustomClassLoader 用戶自定義類加載器
他們的區(qū)別上面也都有說明。需要注意的是,不同的類加載器加載的類是不同的,因此如果用戶加載器1加載的某個(gè)類,其他用戶并不能夠使用。
?
當(dāng)JVM運(yùn)行過程中,用戶需要加載某些類時(shí),會(huì)按照下面的步驟(父類委托機(jī)制):
1 用戶自己的類加載器,把加載請(qǐng)求傳給父加載器,父加載器再傳給其父加載器,一直到加載器樹的頂層。
2 最頂層的類加載器首先針對(duì)其特定的位置加載,如果加載不到就轉(zhuǎn)交給子類。
3 如果一直到底層的類加載都沒有加載到,那么就會(huì)拋出異常ClassNotFoundException。
因此,按照這個(gè)過程可以想到,如果同樣在CLASSPATH指定的目錄中和自己工作目錄中存放相同的class,會(huì)優(yōu)先加載CLASSPATH目錄中的文件。
Tomcat類加載
在tomcat中類的加載稍有不同,如下圖:
當(dāng)tomcat啟動(dòng)時(shí),會(huì)創(chuàng)建幾種類加載器:
1 Bootstrap 引導(dǎo)類加載器?
加載JVM啟動(dòng)所需的類,以及標(biāo)準(zhǔn)擴(kuò)展類(位于jre/lib/ext下)
2 System 系統(tǒng)類加載器?
加載tomcat啟動(dòng)的類,比如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定。位于CATALINA_HOME/bin下。
3 Common 通用類加載器?
加載tomcat使用以及應(yīng)用通用的一些類,位于CATALINA_HOME/lib下,比如servlet-api.jar
4 webapp 應(yīng)用類加載器
每個(gè)應(yīng)用在部署后,都會(huì)創(chuàng)建一個(gè)唯一的類加載器。該類加載器會(huì)加載位于?WEB-INF/lib下的jar文件中的class?和?WEB-INF/classes下的class文件。
?
當(dāng)應(yīng)用需要到某個(gè)類時(shí),則會(huì)按照下面的順序進(jìn)行類加載:
1 使用bootstrap引導(dǎo)類加載器加載
2 使用system系統(tǒng)類加載器加載
3 使用應(yīng)用類加載器在WEB-INF/classes中加載
4 使用應(yīng)用類加載器在WEB-INF/lib中加載
5 使用common類加載器在CATALINA_HOME/lib中加載
?
問題擴(kuò)展
通過對(duì)上面tomcat類加載機(jī)制的理解,就不難明白?為什么java文件放在Eclipse中的src文件夾下會(huì)優(yōu)先jar包中的class?
這是因?yàn)镋clipse中的src文件夾中的文件java以及webContent中的JSP都會(huì)在tomcat啟動(dòng)時(shí),被編譯成class文件放在?WEB-INF/class?中。
而Eclipse外部引用的jar包,則相當(dāng)于放在?WEB-INF/lib?中。
因此肯定是?java文件或者JSP文件編譯出的class優(yōu)先加載。
通過這樣,我們就可以簡單的把java文件放置在src文件夾中,通過對(duì)該java文件的修改以及調(diào)試,便于學(xué)習(xí)擁有源碼java文件、卻沒有打包成xxx-source的jar包。
?
另外呢,開發(fā)者也會(huì)因?yàn)榇中亩赶旅娴腻e(cuò)誤。
在?CATALINA_HOME/lib?以及?WEB-INF/lib?中放置了?不同版本的jar包,此時(shí)就會(huì)導(dǎo)致某些情況下報(bào)加載不到類的錯(cuò)誤。
還有如果多個(gè)應(yīng)用使用同一jar包文件,當(dāng)放置了多份,就可能導(dǎo)致?多個(gè)應(yīng)用間?出現(xiàn)類加載不到的錯(cuò)誤。
2.2 邏輯關(guān)系圖
正在上傳…重新上傳取消正在上傳…重新上傳取消
?
上面說到Common,Catalina,Shared類加載器是URLClassLoader類的一個(gè)實(shí)例,在默認(rèn)的配置中,它們其實(shí)都是同一個(gè)對(duì)象,即commonLoader,結(jié)合初始化時(shí)的代碼(只保留關(guān)鍵代碼):
private void initClassLoaders() {commonLoader = createClassLoader("common", null); // commonLoader的加載路徑為common.loaderif( commonLoader == null ) {commonLoader=this.getClass().getClassLoader();}catalinaLoader = createClassLoader("server", commonLoader); // 加載路徑為server.loader,默認(rèn)為空,父類加載器為commonLoadersharedLoader = createClassLoader("shared", commonLoader); // 加載路徑為shared.loader,默認(rèn)為空,父類加載器為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對(duì)象,默認(rèn)3者為同一個(gè)對(duì)象}?
在上面的代碼初始化時(shí)很明確是指出了,catalina與shared類加載器的父類加載器為common類加載器,而初始化commonClassLoader時(shí)父類加載器設(shè)置為null,最終會(huì)調(diào)到createClassLoader靜態(tài)方法:
public static ClassLoader createClassLoader(List<Repository> repositories,final ClassLoader parent)throws Exception {.....return AccessController.doPrivileged(new PrivilegedAction<URLClassLoader>() {@Overridepublic URLClassLoader run() {if (parent == null)return new URLClassLoader(array); //該構(gòu)造方法默認(rèn)獲取系統(tǒng)類加載器為父類加載器,即AppClassLoaderelsereturn new URLClassLoader(array, parent);}});}?
在createClassLoader中指定參數(shù)parent==null時(shí),最終會(huì)以系統(tǒng)類加載器(AppClassLoader)作為父類加載器,這解釋了為什么commonClassLoader的父類加載器是AppClassLoader.
一個(gè)web應(yīng)用對(duì)應(yīng)著一個(gè)StandardContext實(shí)例,每個(gè)web應(yīng)用都擁有獨(dú)立web應(yīng)用類加載器(WebClassLoader),這個(gè)類加載器在StandardContext.startInternal()中被構(gòu)造了出來:
if (getLoader() == null) {WebappLoader webappLoader = new WebappLoader(getParentClassLoader());webappLoader.setDelegate(getDelegate());setLoader(webappLoader);}這里getParentClassLoader()會(huì)獲取父容器StandarHost.parentClassLoader對(duì)象屬性,而這個(gè)對(duì)象屬性是在Catalina$SetParentClassLoaderRule.begin()初始化,初始化的值其實(shí)就是Catalina.parentClassLoader對(duì)象屬性,再來跟蹤一下Catalina.parentClassLoader,在Bootstrap.init()時(shí)通過反射調(diào)用了Catalina.setParentClassLoader(),將Bootstrap.sharedLoader屬性設(shè)置為Catalina.parentClassLoader,所以WebClassLoader的父類加載器是Shared ClassLoader.
2.3 類加載邏輯
tomcat的類加載機(jī)制是違反了雙親委托原則的,對(duì)于一些未加載的非基礎(chǔ)類(Object,String等),各個(gè)web應(yīng)用自己的類加載器(WebAppClassLoader)會(huì)優(yōu)先加載,加載不到時(shí)再交給commonClassLoader走雙親委托。具體的加載邏輯位于WebAppClassLoaderBase.loadClass()方法中,代碼篇幅長,這里以文字描述加載一個(gè)類過程:
第3第4兩個(gè)步驟的順序已經(jīng)違反了雙親委托機(jī)制,除了tomcat之外,JDBC,JNDI,Thread.currentThread().setContextClassLoader();等很多地方都一樣是違反了雙親委托。
?
總結(jié)
以上是生活随笔為你收集整理的图解Tomcat类加载机制(阿里面试题)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深入理解Java类加载机制
- 下一篇: JMM和底层实现原理