當前位置: 妍妍網 > 碼農

面試官:Tomcat 為什麽要破壞 Java 雙親委派機制?被問傻眼了

2024-01-31碼農

來自: www.jianshu.com/p/abf6fd4531e7

我想,在研究tomcat 類載入之前,我們復習一下或者說鞏固一下java 預設的類載入器。樓主以前對類載入也是懵懵懂懂,借此機會,也好好復習一下。

樓主翻開了神書【 深入理解Java虛擬機器 】第二版,p227, 關於類載入器的部份。請看:

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 目錄中的,或者被-Xboot classpath 參數所指定的路徑種的,並且是虛擬機器辨識的(僅按照檔名辨識,如rt.jar,名字不符合的類別庫即使放在lib目錄下也不會多載)。

  • 擴充套件類載入器(Extension classLoader): 這個類載入器由 sun.misc.Launcher$Ext classLoader 實作,它負責夾雜JAVA_HOME/lib/ext 目錄下的,或者被java.ext.dirs 系統變量所指定的路徑種的所有類別庫。開發者可以直接使用擴充套件類載入器。

  • 應用程式類載入器(Application classLoader): 這個類載入器由 sun.misc.Launcher$App classLoader 實作。由於這個類載入器是 classLoader 種的getSystem classLoader方法的返回值,所以也成為系統類載入器。它負責載入使用者類路徑( classPath)上所指定的類別庫。開發者可以直接使用這個類載入器,如果套用中沒有定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。

  • 這些類載入器之間的關系一般如下圖所示:

    圖中各個類載入器之間的關系成為 類載入器的雙親委派模型(Parents Dlegation Mode)。雙親委派模型要求除了頂層的啟動類載入器之外,其余的類載入器都應當由自己的父類載入器載入,這裏類載入器之間的父子關系一般不會以繼承的關系來實作,而是都使用組合關系來復用父載入器的程式碼。

    類載入器的雙親委派模型在JDK1.2 期間被引入並被廣泛套用於之後的所有Java程式中,但他並不是個強制性的約束模型,而是Java設計者推薦給開發者的一種類載入器實作方式。

    雙親委派模型的工作過程是:如果一個類載入器收到了類載入的請求,他首先不會自己去嘗試載入這個類,而是把這個請求委派父類載入器去完成。每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個請求(他的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。

    為什麽要這麽做呢?

    如果沒有使用雙親委派模型,由各個類載入器自行載入的話,如果使用者自己編寫了一個稱為java.lang.Object的類,並放在程式的 classPath中,那系統將會出現多個不同的Object類, Java型別體系中最基礎的行為就無法保證。應用程式也將會變得一片混亂。

    雙親委任模型時如何實作的?

    非常簡單:所有的程式碼都在 java.lang. classLoader 中的load class方法之中,程式碼如下:

    邏輯清晰易懂:先檢查是否已經被載入過,若沒有載入則呼叫父載入器的load class方法, 如父載入器為空則預設使用啟動類載入器作為父載入器。如果父類載入失敗,丟擲 classNotFoundException 異常後,再呼叫自己的find class方法進行載入。

    剛剛我們說過,雙親委任模型不是一個強制性的約束模型,而是一個建議型的類載入器實作方式。在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 類的setContext classLoader方法進行設定。如果建立執行緒時還未設定,它將會從父執行緒中繼承一個,如果在應用程式的全域範圍內都沒有設定過多的話,那這個類載入器預設即使應用程式類載入器。

    嘿嘿,有了執行緒上下文載入器,JNDI服務使用這個執行緒上下文載入器去載入所需要的SPI程式碼,也就是父類載入器請求子類別載入器去完成類載入的動作,這種行為實際上就是打通了雙親委派模型的階層來逆向使用類載入器,實際上已經違背了雙親委派模型的一般性原則。但這無可奈何,Java中所有涉及SPI的載入動作基本勝都采用這種方式。例如JNDI,JDBC,JCE, JAXB ,JBI等。

    第三次:為了實作熱插拔,熱部署,模組化,意思是添加一個功能或減去一個功能不用重新開機,只需要把這模組連同類載入器一起換掉就實作了程式碼的熱替換。

    書中還說到:

    Java 程式中基本有一個共識: OSGI 對類載入器的使用時值得學習的,弄懂了OSGI的實作,就可以算是掌握了類載入器的精髓。

    牛逼啊!!!

    現在,我們已經基本明白了Java預設的類載入的作用了原理,也知道雙親委派模型。說了這麽多,差點把我們的tomcat給忘了,我們的題目是Tomcat 載入器為何違背雙親委派模型?下面就好好說說我們的tomcat的類載入器。

    4. Tomcat 的類載入器是怎麽設計的?

    首先,我們來問個問題:

    Tomcat 如果使用預設的類載入機制行不行?

    我們思考一下:Tomcat是個web容器, 那麽它要解決什麽問題:

  • 一個web容器可能需要部署兩個應用程式,不同的應用程式可能會依賴同一個第三方類別庫的不同版本,不能要求同一個類別庫在同一個伺服器只有一份,因此要保證每個應用程式的類別庫都是獨立的,保證相互隔離。

  • 部署在同一個web容器中相同的類別庫相同的版本可以共享。否則,如果伺服器有10個應用程式,那麽要有10份相同的類別庫載入進虛擬機器,這是扯淡的。

  • web容器也有自己依賴的類別庫,不能於應用程式的類別庫混淆。基於安全考慮,應該讓容器的類別庫和程式的類別庫隔離開來。

  • web容器要支持jsp的修改,我們知道,jsp 檔最終也是要編譯成 class檔才能在虛擬機器中執行,但程式執行後修改jsp已經是司空見慣的事情,否則要你何用?所以,web容器需要支持 jsp 修改後不用重新開機。

  • 再看看我們的問題:Tomcat 如果使用預設的類載入機制行不行?

    答案是不行的。為什麽?我們看,第一個問題,如果使用預設的類載入器機制,那麽是無法載入兩個相同類別庫的不同版本的,預設的累加器是不管你是什麽版本的,只在乎你的全限定類名,並且只有一份。

    第二個問題,預設的類載入器是能夠實作的,因為他的職責就是保證唯一性。第三個問題和第一個問題一樣。我們再看第四個問題,我們想我們要怎麽實作jsp檔的熱修改(樓主起的名字),jsp 檔其實也就是 class檔,那麽如果修改了,但類名還是一樣,類載入器會直接取方法區中已經存在的,修改後的jsp是不會重新載入的。

    那麽怎麽辦呢?我們可以直接解除安裝掉這jsp檔的類載入器,所以你應該想到了,每個jsp檔對應一個唯一的類載入器,當一個jsp檔修改了,就直接解除安裝這個jsp類載入器。重新建立類載入器,重新載入jsp檔。

    Tomcat 如何實作自己獨特的類載入機制?

    所以,Tomcat 是怎麽實作的呢?牛逼的Tomcat團隊已經設計好了。我們看看他們的設計圖:

    我們看到,前面3個類載入和預設的一致,Common classLoader、Catalina classLoader、Shared classLoader和Webapp classLoader則是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容器不可見;

  • Webapp classLoader :各個Webapp私有的類載入器,載入路徑中的 class只對當前Webapp可見;

  • 從圖中的委派關系中可以看出:

    Common classLoader能載入的類都可以被Catalina classLoader和Shared classLoader使用,從而實作了公有類別庫的共用,而Catalina classLoader和Shared classLoader自己能載入的類則與對方相互隔離。

    WebApp classLoader可以使用Shared classLoader載入到的類,但各個WebApp classLoader例項之間相互隔離。

    而JasperLoader的載入範圍僅僅是這個JSP檔所編譯出來的那一個. class檔,它出現的目的就是為了被丟棄:當Web容器檢測到JSP檔被修改時,會替換掉目前的JasperLoader的例項,並透過再建立一個新的Jsp類載入器來實作JSP檔的 HotSwap 功能。

    好了,至此,我們已經知道了tomcat為什麽要這麽設計,以及是如何設計的,那麽,tomcat 違背了java 推薦的雙親委派模型了嗎?答案是:違背了。

    我們前面說過:

    雙親委派模型要求除了頂層的啟動類載入器之外,其余的類載入器都應當由自己的父類載入器載入。

    很顯然,tomcat 不是這樣實作,tomcat 為了實作隔離性,沒有遵守這個約定,每個webapp classLoader載入自己的目錄下的 class檔,不會傳遞給父類載入器。

    我們擴充套件出一個問題:如果tomcat 的 Common classLoader 想載入 WebApp classLoader 中的類,該怎麽辦?看了前面的關於破壞雙親委派模型的內容,我們心裏有數了,我們可以使用執行緒上下文類載入器實作,使用執行緒上下文載入器,可以讓父類載入器請求子類別載入器去完成類載入的動作。牛逼吧。

    總結

    好了,終於,我們明白了Tomcat 為何違背雙親委派模型,也知道了tomcat的類載入器是如何設計的。順便復習了一下 Java 預設的類載入器機制,也知道了如何破壞Java的類載入機制。這一次收獲不小哦!!!嘿嘿。