1. 程式人生 > >類載入器在Tomcat中的應用

類載入器在Tomcat中的應用

之前有文章已經介紹過了JVM中的類載入機制,JVM中通過類載入載入class檔案,通過雙親委派模型完成分層載入。實際上類載入機制並不僅僅是在JVM中得以運用,通過影響位元組碼生成和類載入器目前已經有了許多相關的技術誕生。特別的對於進行應用伺服器的開發過程中,類載入機制幾乎是必須掌握的。

為什麼在Tomcat中需要自定義類載入器

做Java開發的肯定都有用過tomcat,回想一下我們使用tomcat是的場景。最初的時候使用tomcat大多都是單純的使用其作為專案的容器,而沒有考慮多著中間的很多問題。

隔離

最初我們使用都是在一臺tomcat上部署多個專案,然後通過埠進行區分。那麼這裡就存在著一個問題,我們都知道一臺tomcat會啟動一個JVM,部署在tomcat 的應用實際上是跑在這一個jvm中的。

相信大家多多少少都遇到過jar包衝突的問題。不同的專案會有不同的依賴jar包,可能是不同型別的包,也可能是相同包的不同版本問題。前一文章中又提到jvm校驗類的唯一性是通過類載入器+全限定名完成的。如果只是前文中提到的幾中類載入器的話那麼在載入多個專案時肯定會出現不同版本的jar包覆蓋的情況,那麼程式一跑起來就報丟擲ClassNotFoundException的異常。

那麼很明顯tomcat需要對不同的專案進行隔離,保證各個專案不會相互影響,這正好可以通過類載入器來完成。

共享

上面講到了不同專案間需要進行隔離,而除了隔離還需要考慮共享的問題。Java開發中其臃腫的體系一直為人詬病,一個很小的web一個專案會有這一大堆的依賴包。如果在一臺tomcat中部署多個專案,每個專案都需要單獨將依賴類載入到JVM中。這樣導致的後果一是佔據了一部分儲存空間;二是消耗了大量的記憶體,每個專案都單獨載入一次,在啟動tomcat後可能會存在大量重複的Class物件(這裡的重複不是JVM意義上的重複,只是說來源於同一個包),那麼專案執行可用記憶體變少使得GC變得頻繁。

解決這個問題我們可以將多個專案中共同依賴的包抽出來讓JVM只加載一次。這樣既不會佔據多的儲存空間,對記憶體的消耗也將減少。

效能

tomcat作為一個應用伺服器效能肯定是非常需要考慮的事情,我們都知道JVM在載入Class檔案時是沒有辦法直接定位到一個具體的檔案的,只能搜尋指定目錄下的的jar包的內容,那麼這裡的問題就是如果很多的web專案依賴了大量的jar檔案,這時要載入一個Class檔案就可能會搜尋這所有的jar才能載入到該類,這樣對效能的影響就很大了。

HotSwap

hotSwap也叫熱載入,之前在查詢資料的時候發現不少文章都沒弄清楚熱載入和熱部署的區別,這裡簡單說笑。

/ 熱部署 熱載入
實現方式 發生修改時整個專案重新打包部署 只替換被修改過的類
使用場景 生產可以使用 開發時使用,因為開發中需要頻繁的除錯程式碼

熱部署是在當代碼被修改後重新打包整個專案後重新部署,實現方式有多種,目前很多應用伺服器都有這個功能,還有一些第三方工具也可以做到。但是不建議在開發中開啟該功能,很多人不明白熱部署和熱載入的區別,結果在開發時使用了熱部署結果導致可能沒修改一次程式碼然後就後臺跑一個打包部署的程式,使得電腦變得很卡。

熱替換的典型應用就是對於JSP應用的運行了。我們都知道JSP應用執行是先將JSP翻譯為Servlet.java,然後將.java編譯為.class檔案。同時由於jsp中將邏輯程式碼和頁面放在一起了,其修改的概率很高,如果每次修改都需要手動重啟專案那會嚴重影響開發效率,但如果使用熱部署確實能解決問題,但也會電腦卡死的風險。所以我們希望的是在修改後能夠只重新載入被修改過的檔案,其他沒有修改的不懂。

需要哪些類載入器

通過上面的問題我們可以大概的整理出一個tomcat中的類載入模型。

首先JVM中已經定義好的幾個類載入器肯定是不能少的。

如何實現隔離性?

要實現隔離我們得先理清哪些地方需要進行隔離。

首先部署的不同的專案之間肯定是需要進行隔離的,防止出現包覆蓋,各個專案相互之間要隔離那肯定就需要每個專案有一個獨屬於自己的類載入器,這樣才能夠保證類載入器+全限定名的唯一性。

其次呢我們知道tomcat本身也是Java語言進行開發的,那麼它本身肯定也會有依賴的jar包,那麼載入tomcat依賴也需要一個類載入器。為什麼載入tomcat依賴不能使用JVM提供的類載入器呢?因為假如使用了JVM的類載入器載入了jar包後,如果其他專案有依賴相同的jar,那麼根據雙親委派模型又會存在包覆蓋的問題了。

到這裡我們知道了每個web專案需要一個類載入器,載入tomcat依賴包也需要一個類載入器。

如何實現共享性?

為了利用好JVM的記憶體,共享依賴的jar也是很有必要的,在實現隔離性時對每一個web專案都準備了一個類載入器。而共享是要求在載入共享的這一部分jar檔案時不從當前專案中載入,而是使用共享的jar檔案。

載入共享的jar肯定也需要一個類載入器,根據其使用的特性來說載入共享jar的類載入器和web專案的類載入器還存在著一個層級關係。

熱部署和熱載入?

熱部署是重新載入整個專案,那就很明朗了,如果重新載入那就代表以前載入的就失效了,我們可以直接廢棄掉之前專案的類載入器,tomcat重新生成一個新的載入當前專案的類載入器即可。

熱載入就麻煩點了,熱載入要求在修改檔案後不重新打包部署專案也能夠直接使用。那麼直接重新載入整個專案就不可取了。

一種方法是首先解除安裝掉被修改的檔案咋JVM中已經存在的Class,然後重新載入修改後的檔案。理論上來說這是可行的,但是實際上Class的解除安裝條件本身及其苛刻,而且Class的解除安裝時有GC完成的,我們沒有辦法主動的完成解除安裝的這個過程,所以這一方法就不可行了。

另一個方式就是給每一個jsp建立一個類載入器,這個類載入器只負責這一個jsp,當檔案被修改後將這一個類載入器無效,然後重新建立一個新的類載入器來載入即可。

而對每一個web專案都建立一個類載入器使得在載入時也不會去從別的專案中搜索jar,就不會存在每次載入好事很久的問題了。

類載入結構

根據上面的分析我們可以得出一個tomcat中的類載入結構。

最上層是由JVM提供的幾個預設的類載入器,tomcat類載入器來載入tomcat本身需要的依賴包,共享jar類載入器載入在不同專案間共同依賴的jar。

實際的tomcat的類載入結構如下圖:

想較與我們自己設計的多了一個common Classloader,在預設情況下我們並不會使用到共享的功能,基本上都是由web類載入器來載入整個專案,所以預設情況下tomcat都不開啟shared classloader和Catalina classloader,不開啟的情況下預設都將所有這部分功能有common classloader代替。

破壞雙親委派模型

這裡有一個問題需要討論的是tomcat這種類載入模型是否破壞了雙親委派模型呢?

答案是肯定的,要實現上面的功能,那麼根據jar的功能其肯定是被其指定的classloader進行載入而不會繼續往上推(比如共享的jar在Shared Classloader就直接載入了而不會繼續推向Common Classloader)。這明顯是不符合雙親委派模型的。

而且在tomcat的類載入模型中,假設一部分被共享的jar由Shared Classloader進行載入(比如spring)。我們都知道spring作為一個ioc容器必然是需要訪問到我們web應用中的類的,如果要在spring中載入需要的類這時使用的classloader就是載入spring的classloader,而使用者程式顯然是放在/WEB-INF目錄中的,載入該目錄的classloader卻是web classloader。在上面的類載入層級關係中我們可以看到這兩個載入器是上下級關係,但是雙親委派模型中要求向上查詢而不能向下查詢,那麼很明顯如果遵循雙親委派模型的話功能就無法正常運行了。

解決這個問題也很簡單,JVM團隊提供了一個執行緒上下文類載入器( Thread Context Class Loader)。這個類載入器可以通過Thread類的setContextClassLoaserO方法進行設定,使用該方法就可以讓父類載入器請求子類載入器去完成類載入的動作,這也會打破雙親委派模型的層次結構來逆向使用類載入器。

解決上面問題就是通過spring使用執行緒上下文載入器來載入類,而執行緒上下文載入器預設設定為了WebAppClassLoader,那麼這時是哪一個Web應用呼叫了spring,spring就會用該應用的WebAppClassLoader來載入需要的bean。

如何使用

Tomcat預設情況下僅使用common classloader來載入自身的依賴和共享的依賴。如果我們要啟用Catalina Classloader或者Shared Calssloader需要自己進行手動配置。

下圖為Tomcat8.0的目錄結構:

預設情況下Common ClassLoader載入的jar都放在lib包下。

如果我們要進行配置需要找到conf/catalina.properties檔案。找到common.loader,server.loader,shared.loader三個引數,這三個引數分別設定Common ClassLoader,Catalina ClassLoader,Shared ClassLoader的載入的jar包的位置。

引數值可以是一個指定目錄下的所有包,可以是指定的jar包,也可以是包名符合一定規則的jar包。

該配置檔案還可以配置哪些指定名稱的jar包不進行載入。

熱載入or熱部署

要想使用熱載入或者熱部署也需要修改配置檔案,在conf/Catalina/localhost資料夾下新建一個xml檔案,設定內容為:

//熱載入
//docBase指專案路徑,可以使用絕對路徑或相對路徑,相對路徑是相對於webapps 
<Context docBase="D:\demo\WebRoot" path="/demo" reloadable="true"/>
//熱部署
//熱部署只需要將reloadable置為false即可,這裡存在一個屬性autoDeploy預設為true,表示支援熱部署
<Context docBase="D:\demo\WebRoot" path="/demo" reloadable="false"/>

其實並不一定都需要新增新的xml檔案,我們也可以找到conf/server.xml中在

//<Host>標籤中autoDeploy=true,表示預設支援熱部署,
//這樣只要tomcat在執行中,我們將war包放入到webapps下tomcat就會幫我們自動的部署
<Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
            </Host>