這是我們研究Tomcat的第四篇文章,前三篇文章我們搭建了原始碼框架,瞭解了tomcat的大致的設計架構, 還寫了一個簡單的伺服器。按照我們最初訂的計劃,今天,我們要開始研究tomcat的幾個主要元件(元件太多,無法一一解析,解析幾個核心),包括核心的類載入器,聯結器和容器,還有生命週期,還有pipeline 和 valve。一個一個來,今天來研究類載入器。
我們分為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容器, 那麼它要解決什麼問題:
- 一個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個類載入和預設的一致,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(五)Tomcat 兩大核心元件----聯結器和容器!!!
good luck!!!