我們分為4個部分來探討:
1. 什么是類加載機制?
2. 什么是雙親委任模型?
3. 如何破壞雙親委任模型?
4. Tomcat 的類加載器是怎么設計的?
我想,在研究tomcat 類加載之前,我們復習一下或者說鞏固一下java 默認的類加載器。樓主以前對類加載也是懵懵懂懂,借此機會,也好好復習一下。
樓主翻開了神書《深入理解Java虛擬機》第二版,p227, 關於類加載器的部分。請看:
1. 什么是類加載機制?
代碼編譯的結果從本地機器碼轉變成字節碼,是存儲格式的一小步,卻是編程語言發展的一大步。
Java虛擬機把描述類的數據從Class文件加載進內存,並對數據進行校驗,轉換解析和初始化,最終形成可以唄虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。
虛擬機設計團隊把類加載階段中的“通過一個類的全限定名來獲取描述此類的二進制字節流”這個動作放到Java虛擬機外部去實現,以便讓應用程序自己決定如何去獲取所需要的類。實現這動作的代碼模塊成為“類加載器”。
類與類加載器的關系
類加載器雖然只用於實現類的加載動作,但它在Java程序中起到的作用卻遠遠不限於類加載階段。對於任意一個類,都需要由加載他的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類命名空間。這句話可以表達的更通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來自同一個Class文件,被同一個虛擬機加載,只要加載他們的類加載器不同,那這個兩個類就必定不相等。
2. 什么是雙親委任模型
從Java虛擬機的角度來說,只存在兩種不同類加載器:一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現(只限HotSpot),是虛擬機自身的一部分;另一種就是所有其他的類加載器,這些類加載器都由Java語言實現,獨立於虛擬機外部,並且全都繼承自抽象類java.lang.ClassLoader.
從Java開發人員的角度來看,類加載還可以划分的更細致一些,絕大部分Java程序員都會使用以下3種系統提供的類加載器:
啟動類加載器(Bootstrap ClassLoader):這個類加載器復雜將存放在 JAVA_HOME/lib 目錄中的,或者被-Xbootclasspath 參數所指定的路徑種的,並且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即使放在lib目錄下也不會重載)。
擴展類加載器(Extension ClassLoader):這個類加載器由sun.misc.Launcher$ExtClassLoader實現,它負責夾雜JAVA_HOME/lib/ext 目錄下的,或者被java.ext.dirs 系統變量所指定的路徑種的所有類庫。開發者可以直接使用擴展類加載器。
應用程序類加載器(Application ClassLoader):這個類加載器由sun.misc.Launcher$AppClassLoader 實現。由於這個類加載器是ClassLoader 種的getSystemClassLoader方法的返回值,所以也成為系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫。開發者可以直接使用這個類加載器,如果應用中沒有定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
這些類加載器之間的關系一般如下圖所示:
圖中各個類加載器之間的關系成為 類加載器的雙親委派模型(Parents Dlegation Mode)。雙親委派模型要求除了頂層的啟動類加載器之外,其余的類加載器都應當由自己的父類加載器加載,這里類加載器之間的父子關系一般不會以繼承的關系來實現,而是都使用組合關系來復用父加載器的代碼。
類加載器的雙親委派模型在JDK1.2 期間被引入並被廣泛應用於之后的所有Java程序中,但他並不是個強制性的約束模型,而是Java設計者推薦給開發者的一種類加載器實現方式。
雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求,他首先不會自己去嘗試加載這個類,而是把這個請求委派父類加載器去完成。每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個請求(他的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。
為什么要這么做呢?
如果沒有使用雙親委派模型,由各個類加載器自行加載的話,如果用戶自己編寫了一個稱為java.lang.Object的類,並放在程序的ClassPath中,那系統將會出現多個不同的Object類, Java類型體系中最基礎的行為就無法保證。應用程序也將會變得一片混亂。
雙親委任模型時如何實現的?
非常簡單:所有的代碼都在java.lang.ClassLoader中的loadClass方法之中,代碼如下:
邏輯清晰易懂:先檢查是否已經被加載過,若沒有加載則調用父加載器的loadClass方法, 如父加載器為空則默認使用啟動類加載器作為父加載器。如果父類加載失敗,拋出ClassNotFoundException 異常后,再調用自己的findClass方法進行加載。
3. 如何破壞雙親委任模型?
剛剛我們說過,雙親委任模型不是一個強制性的約束模型,而是一個建議型的類加載器實現方式。在Java的世界中大部分的類加載器都遵循者模型,但也有例外,到目前為止,雙親委派模型有過3次大規模的“被破壞”的情況。
第一次:在雙親委派模型出現之前—–即JDK1.2發布之前。
第二次:是這個模型自身的缺陷導致的。我們說,雙親委派模型很好的解決了各個類加載器的基礎類的統一問題(越基礎的類由越上層的加載器進行加載),基礎類之所以稱為“基礎”,是因為它們總是作為被用戶代碼調用的API, 但沒有絕對,如果基礎類調用會用戶的代碼怎么辦呢?
這不是沒有可能的。一個典型的例子就是JNDI服務,JNDI現在已經是Java的標准服務,它的代碼由啟動類加載器去加載(在JDK1.3時就放進去的rt.jar),但它需要調用由獨立廠商實現並部署在應用程序的ClassPath下的JNDI接口提供者(SPI, Service Provider Interface)的代碼,但啟動類加載器不可能“認識“這些代碼啊。因為這些類不在rt.jar中,但是啟動類加載器又需要加載。怎么辦呢?
為了解決這個問題,Java設計團隊只好引入了一個不太優雅的設計:線程上下文類加載器(Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContextClassLoader方法進行設置。如果創建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局范圍內都沒有設置過多的話,那這個類加載器默認即使應用程序類加載器。
嘿嘿,有了線程上下文加載器,JNDI服務使用這個線程上下文加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載的動作,這種行為實際上就是打通了雙親委派模型的層次結構來逆向使用類加載器,實際上已經違背了雙親委派模型的一般性原則。但這無可奈何,Java中所有涉及SPI的加載動作基本勝都采用這種方式。例如JNDI,JDBC,JCE,JAXB,JBI等。
第三次:為了實現熱插拔,熱部署,模塊化,意思是添加一個功能或減去一個功能不用重啟,只需要把這模塊連同類加載器一起換掉就實現了代碼的熱替換。
書中還說到:
Java 程序中基本有一個共識:OSGI對類加載器的使用時值得學習的,弄懂了OSGI的實現,就可以算是掌握了類加載器的精髓。
牛逼啊!!!
現在,我們已經基本明白了Java默認的類加載的作用了原理,也知道雙親委派模型。說了這么多,差點把我們的tomcat給忘了,我們的題目是Tomcat 加載器為何違背雙親委派模型?下面就好好說說我們的tomcat的類加載器。
4. Tomcat 的類加載器是怎么設計的?
首先,我們來問個問題:
Tomcat 如果使用默認的類加載機制行不行?
我們思考一下: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 中的類,該怎么辦?
看了前面的關於破壞雙親委派模型的內容,我們心里有數了,我們可以使用線程上下文類加載器實現,使用線程上下文加載器,可以讓父類加載器請求子類加載器去完成類加載的動作。牛逼吧。
總結
好了,終於,我們明白了Tomcat 為何違背雙親委派模型,也知道了tomcat的類加載器是如何設計的。順便復習了一下 Java 默認的類加載器機制,也知道了如何破壞Java的類加載機制。這一次收獲不小哦!!! 嘿嘿。
好了,今天到此為止。
————————————————
原文鏈接:https://blog.csdn.net/qq_38182963/article/details/78660779