之前有文章已經介紹過了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>