前幾天,一位朋友在面試中被問到類別載入的問題,常規的回答類別載入相關問題,只要稍微背背面試題都能應付。
但是,要深一點問,就深一點點,就能涼涼一大批人。
就比方說:tomcat 類別載入器為什麼要違背雙親委派模型?
為了不讓你在面試中也遇到這類情況,本文就安排這個事。
我們分成4個部分來探討:
我想,在研究tomcat 類別載入之前,我們先複習一下或者說鞏固java 預設的類別載入器。剛開始學JVM的時候,對類別載入機制也是懵懵懂懂,藉此機會跟大家分享一波。
程式碼編譯的結果從本機機器碼轉變成字節碼,是儲存格式的一小步,卻是程式語言發展的一大步。
Java虛擬機器把描述類別的資料從Class檔案載入進內存,並對資料進行校驗,轉換解析與初始化,最終形成可以被虛擬機器直接使用的Java類型,這就是虛擬機器的類別載入機制。
虛擬機設計團隊把類別載入階段中的「透過一個類別的全限定名來取得描述此類的二進位位元組流」這個動作放到Java虛擬機器外部去實現,以便讓應用程式自己決定如何去取得所需的類別。實現這動作的程式碼模組成為「類別載入器」。
類別載入器雖然只用於實作類別的載入動作,但它在Java程式中所扮演的角色遠遠不限於類別載入階段。對於任意一個類,都需要由載入他的類別載入器和這個類別本身一同確立其在Java虛擬機器中的唯一性,每一個類別載入器,都擁有一個獨立的類別命名空間。
這句話可以表達的更通俗一些:比較兩個類別是否“相等”,只有在這兩個類別是由同一個類別載入器載入的前提下才有意義,否則,即使這兩個類別來自同一個Class文件,被同一個虛擬機器加載,只要加載他們的類別加載器不同,那麼這個兩個類別就必定不相等。
1.從Java虛擬機的角度來說,只存在兩種不同類別載入器:一種是啟動類別載入器(Bootstrap ClassLoader) ,這個類別載入器使用C 語言實作(只限HotSpot),是虛擬機器本身的一部分;另一種就是所有其他的類別載入器,這些類別載入器都由Java語言實現,獨立於虛擬機器外部,並且全都繼承自抽象類別java.lang.ClassLoader
.
2.從Java開發人員的角度來看,類別載入還可以分割的更細緻一些,絕大部分Java程式設計師都會使用以下3種系統提供的類別載入器:
sun.misc.Launcher$ExtClassLoader
實現,它負責夾雜JAVA_HOME/lib/ext 目錄下的,或是被java.ext.dirs 系統變數所指定的路徑中的所有類別庫。開發者可以直接使用擴充類別載入器。 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方法進行載入。
剛剛我們說過,雙親委任模型不是一個強制性的約束模型,而是一個建議型的類別載入器實作方式。在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的類別載入器。
首先,我們來問個問題:
Tomcat 如果使用預設的類別載入機制行不行?
我們思考一下:Tomcat是個web容器, 那麼它要解決什麼問題:
再看看我們的問題: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 不是這樣實現,tomcat 為了實現隔離性,沒有遵守這個約定,每個webappClassLoader載入自己的目錄下的class文件,不會傳遞給父類別載入器。
我們擴充出一個問題:如果tomcat 的 Common ClassLoader 想載入 WebApp ClassLoader 中的類,該怎麼辦?看了前面的關於破壞雙親委派模型的內容,我們心裡有數了,我們可以使用線程上下文類加載器實現,使用線程上下文加載器,可以讓父類加載器請求子類加載器去完成類加載的動作。
是不是 很屌?
好了,終於,我們明白了Tomcat 為何違背雙親委派模型,也知道了tomcat的類別載入器是如何設計的。順便複習了一下 Java 預設的類別載入器機制,也知道如何破壞Java的類別載入機制。這次收穫不小! ! !
以上是面試官:tomcat 類別載入器為什麼要違背雙親委派模型?的詳細內容。更多資訊請關注PHP中文網其他相關文章!