Tomcat類加載器爲何違背雙親委派模型

我們分爲4個部分來探討:

   1. 什麼是類加載機制?
   2. 什麼是雙親委任模型?
   3. 如何破壞雙親委任模型?
   4. Tomcat 的類加載器是怎麼設計的?

1、什麼是類加載機制

       Java虛擬機把描述類的數據從Class文件加載進內存,並對數據進行校驗,轉換解析和初始化,最終形成可以唄虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。

       虛擬機設計團隊把類加載階段中的“通過一個類的全限定名來獲取描述此類的二進制字節流”這個動作放到Java虛擬機外部去實現,以便讓應用程序自己決定如何去獲取所需要的類。實現這動作的代碼模塊成爲“類加載器”。

類與類加載器的關係:

類加載器雖然只用於實現類的加載動作,但它在Java程序中起到的作用卻遠遠不限於類加載階段。對於任意一個
類,都需要由加載他的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性,每一個類加載器,都擁有一個
獨立的類命名空間。這句話可以表達的更通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器
加載的前提下才有意義,否則,即使這兩個類來自同一個Class文件,被同一個虛擬機加載,只要加載他們的類加
載器不同,那這個兩個類就必定不相等。

2、什麼是雙親委派模型

  1. 從Java虛擬機的角度來說,只存在兩種不同類加載器:一種是啓動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現(只限HotSpot),是虛擬機自身的一部分;另一種就是所有其他的類加載器,這些類加載器都由Java語言實現,獨立於虛擬機外部,並且全都繼承自抽象類java.lang.ClassLoader.

  2. 從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默認的類加載的作用了原理,也知道雙親委派模型。說了這麼多,差點把我們的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團隊已經設計好了。我們看看他們的設計圖:

我們看到,前面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 的 Common ClassLoader 想加載 WebApp ClassLoader 中的類,該怎麼辦?

看了前面的關於破壞雙親委派模型的內容,我們心裏有數了,我們可以使用線程上下文類加載器實現,使用線程上下文加載器,可以讓父類加載器請求子類加載器去完成類加載的動作。牛逼吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章