咱們分爲4個部分來探討:java
1. 什麼是類加載機制?
2. 什麼是雙親委任模型?
3. 如何破壞雙親委任模型?
4. Tomcat 的類加載器是怎麼設計的?程序員
我想,在研究tomcat 類加載以前,咱們複習一下或者說鞏固一下java 默認的類加載器。樓主之前對類加載也是懵懵懂懂,藉此機會,也好好複習一下。web
樓主翻開了神書《深刻理解Java虛擬機》第二版,p227, 關於類加載器的部分。請看:編程
1. 什麼是類加載機制?
代碼編譯的結果從本地機器碼轉變成字節碼,是存儲格式的一小步,倒是編程語言發展的一大步。tomcat
Java虛擬機把描述類的數據從Class文件加載進內存,並對數據進行校驗,轉換解析和初始化,最終造成能夠唄虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。安全
虛擬機設計團隊把類加載階段中的「經過一個類的全限定名來獲取描述此類的二進制字節流」這個動做放到Java虛擬機外部去實現,以便讓應用程序本身決定如何去獲取所須要的類。實現這動做的代碼模塊成爲「類加載器」。服務器
類與類加載器的關係
類加載器雖然只用於實現類的加載動做,但它在Java程序中起到的做用卻遠遠不限於類加載階段。對於任意一個類,都須要由加載他的類加載器和這個類自己一同確立其在Java虛擬機中的惟一性,每個類加載器,都擁有一個獨立的類命名空間。這句話能夠表達的更通俗一些:比較兩個類是否「相等」,只有在這兩個類是由同一個類加載器加載的前提下才有意義,不然,即便這兩個類來自同一個Class文件,被同一個虛擬機加載,只要加載他們的類加載器不一樣,那這個兩個類就一定不相等。app
2. 什麼是雙親委任模型
從Java虛擬機的角度來講,只存在兩種不一樣類加載器:一種是啓動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現(只限HotSpot),是虛擬機自身的一部分;另外一種就是全部其餘的類加載器,這些類加載器都由Java語言實現,獨立於虛擬機外部,而且全都繼承自抽象類java.lang.ClassLoader.webapp
從Java開發人員的角度來看,類加載還能夠劃分的更細緻一些,絕大部分Java程序員都會使用如下3種系統提供的類加載器:jsp
啓動類加載器(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