(轉載)Java類加載機制(講道理分析的賊牛逼)

Java類加載機制

 

類加載器

  

      虛擬機設計團隊把類加載階段中的“通過一個類的全限定名來獲取描述此類的二進制字節流”這個動作放到Java虛擬機外部去實現,以便讓應用程序自己決定如何去獲取所需要的類。實現這個動作的代碼模塊稱爲“類加載器”

      類加載器可以說是Java語言的一項創新,也是Java語言流行的重要原因之一,它最初是爲了滿足Java Applet的需求而開發出來的。雖然目前Java Applet技術基本上已經“死掉”,但類加載器卻在類層次劃分、OSGi、熱部署、代碼加密等領域大放異彩,成爲了Java技術體系中一塊重要的基石,可謂是失之桑榆,收之東隅。

      類加載器雖然只用於實現類的加載動作,但它在Java程序中起到的作用卻遠遠不限於類加載階段。對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性,每一個類,都擁有一個獨立的類名稱空間。這句話可以表達得更通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義。否則,即使這兩個類來源於同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等

 

 

雙親委派模型

 

 

      從Java虛擬機的角度來講,只存在兩種不同的類加載器:一種是啓動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現,是虛擬機自身的一部分;另一種就是所有其他的類加載器,這些類加載器都由Java語言實現,獨立於虛擬機外部,並且全都繼承自抽象類java.lang.ClassLoader。

      從Java開發人員的角度來看, 類加載器還可以劃分得更細緻一些, 絕大部分Java程序都會使用到以下3種系統提供的類加載器。

1)啓動類加載器(Bootstrap ClassLoader):前面已經介紹過,這個類加載器負責將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中的,並且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。啓動類加載器無法被Java程序直接引用

2)擴展類加載器(Extension ClassLoader):這個加載器由sun.misc.Launcher.ExtClassLoader實現,它負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器

3)應用程序類加載器(Application ClassLoader):這個類加載器由sun.misc.Launcher.AppClassLoader實現。由於這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它爲系統類加載器。它負責加載用戶類路徑(Class Path)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器

      我們的應用程序都是由這3種類加載器互相配合進行加載的,如果有必要,還可以加入自己定義的類加載器。這些類加載器之間的關係一般如下圖所示。

 

      圖中展示的類加載器之間的這種層次關係,稱爲類加載器的雙親委派模型(Parents Delegation Model)。雙親委派模型要求除了頂層的啓動類加載器外,其餘的類加載器都應當有自己的父類加載器。這裏類加載器之間的父子關係一般不會以繼承(Inheritance)的關係來實現,而是都使用組合(Composition)關係來複用父加載器的代碼。類加載器的雙親委派模型在JDK 1.2期間被引入並被廣泛應用於之後幾乎所有的Java程序中,但它並不是一個強制性的約束模型,而是Java設計者推薦給開發者的一種類加載器實現方式。

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

      使用雙親委派模型來組織類加載器之間的關係,有一個顯而易見的好處就是Java類隨着它的類加載器一起具備了一種帶有優先級的層次關係。例如類java.lang.Object,它存放在rt.jar之中,無論哪一個類加載器要加載這個類,最終都是委派給處於模型最頂端的啓動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個稱爲java.lang.Object的類,並放在程序的Class Path中,那系統中將會出現多個不同的Object類,Java類型體系中最基礎的行爲也就無法保證,應用程序也將會變得一片混亂。如果讀者有興趣的話,可以嘗試去編寫一個與rt.jar類庫中已有類重名的Java類,將會發現可以正常編譯,但永遠無法被加載運行。

      雙親委派模型對於保證Java程序的穩定運作很重要,但它的實現卻非常簡單,實現雙親委派的代碼都集中在java.lang.ClassLoader的loadClass()方法之中,如以下代碼所示,邏輯清晰易懂:

 


 
  1. protected synchronized Class<?> loadClass(String name, boolean resolve)

  2. throws ClassNotFoundException

  3. {

  4. //首先, 檢查請求的類是否已經被加載過了

  5. Class c=findLoadedClass(name);

  6. if( c== null ){

  7. try{

  8. if( parent != null ){

  9. c = parent.loadClass(name,false);

  10. } else {

  11. c = findBootstrapClassOrNull(name);

  12. }

  13. } catch (ClassNotFoundException e) {

  14. //如果父類加載器拋出ClassNotFoundException

  15. //說明父類加載器無法完成加載請求

  16. }

  17. if( c == null ) {

  18. //在父類加載器無法加載的時候

  19. //再調用本身的findClass方法來進行類加載

  20. c = findClass(name);

  21. }

  22. }

  23. if(resolve){

  24. resolveClass(c);

  25. }

  26. return c;

  27. }

      先檢查是否已經被加載過,若沒有加載則調用父加載器的loadClass()方法,若父加載器爲空則默認使用啓動類加載器作爲父加載器。如果父類加載失敗,拋出ClassNotFoundException異常後,再調用自己的findClass()方法進行加載。雙親委派的具體邏輯就實現在這個loadClass()方法之中,JDK 1.2之後已不提倡用戶再去覆蓋loadClass()方法,而應當把自己的類加載邏輯寫到findClass()方法中,在loadClass()方法的邏輯裏如果父類加載失敗,則會調用自己的findClass()方法來完成加載,這樣就可以保證新寫出來的類加載器是符合雙親委派規則的。

 

 

打破雙親委派模型

 

 

      上文提到過雙親委派模型並不是一個強制性的約束模型,而是Java設計者推薦給開發者的類加載器實現方式。在Java的世界中大部分的類加載器都遵循這個模型,但也有例外。

      雙親委派模型的一次“被破壞”是由這個模型自身的缺陷所導致的,雙親委派很好地解決了各個類加載器的基礎類的統一問題(越基礎的類由越上層的加載器進行加載)基礎類之所以稱爲“基礎”,是因爲它們總是作爲被用戶代碼調用的API,但世事往往沒有絕對的完美,如果基礎類又要調用回用戶的代碼,那該怎麼辦?這並非是不可能的事情,一個典型的例子便是JNDI服務,JNDI現在已經是Java的標準服務,它的代碼由啓動類加載器去加載(在JDK 1.3時放進去的rt.jar),但JNDI的目的就是對資源進行集中管理和查找,它需要調用由獨立廠商實現並部署在應用程序的Class Path下的JNDI接口提供者(SPI,Service Provider Interface)的代碼,但啓動類加載器不可能“認識”這些代碼,因爲啓動類加載器的搜索範圍中找不到用戶應用程序類,那該怎麼辦?爲了解決這個問題,Java設計團隊只好引入了一個不太優雅的設計:線程上下文類加載器(Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設置,如果創建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局範圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器(Application ClassLoader)。

      有了線程上下文類加載器,就可以做一些“舞弊”的事情了,JNDI服務使用這個線程上下文類加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載的動作這種行爲實際上就是打通了雙親委派模型的層次結構來逆向使用類加載器,實際上已經違背了雙親委派模型的一般性原則,但這也是無可奈何的事情。Java中所有涉及SPI的加載動作基本上都採用這種方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

      雙親委派模型的另一次“被破壞”是由於用戶對程序動態性的追求而導致的,這裏所說的“動態性”指的是當前一些非常“熱門”的名詞:代碼熱替換(HotSwap)、模塊熱部署(HotDeployment)等,說白了就是希望應用程序能像我們的計算機外設那樣,接上鼠標、U盤,不用重啓機器就能立即使用,鼠標有問題或要升級就換個鼠標,不用停機也不用重啓。對於個人計算機來說,重啓一次其實沒有什麼大不了的,但對於一些生產系統來說,關機重啓一次可能就要被列爲生產事故,這種情況下熱部署就對軟件開發者,尤其是企業級軟件開發者具有很大的吸引力。Sun公司所提出的JSR-294、JSR-277規範在與JCP組織的模塊化規範之爭中落敗給JSR-291(即OSGi R4.2),雖然Sun不甘失去Java模塊化的主導權,獨立在發展Jigsaw項目,但目前OSGi已經成爲了業界“事實上”的Java模塊化標準,而OSGi實現模塊化熱部署的關鍵則是它自定義的類加載器機制的實現。每一個程序模塊(OSGi中稱爲Bundle)都有一個自己的類加載器,當需要更換一個Bundle時,就把Bundle連同類加載器一起換掉以實現代碼的熱替換。

      在OSGi環境下,類加載器不再是雙親委派模型中的樹狀結構,而是進一步發展爲更加複雜的網狀結構,當收到類加載請求時,OSGi將按照下面的順序進行類搜索:

1)將以java.*開頭的類委派給父類加載器加載。

2)否則,將委派列表名單內的類委派給父類加載器加載。

3)否則,將Import列表中的類委派給Export這個類的Bundle的類加載器加載。

4)否則,查找當前Bundle的Class Path,使用自己的類加載器加載。

5)否則,查找類是否在自己的Fragment Bundle中,如果在,則委派給Fragment Bundle的類加載器加載。

6)否則,查找Dynamic Import列表的Bundle,委派給對應Bundle的類加載器加載。

7)否則,類查找失敗。

      上面的查找順序中只有開頭兩點仍然符合雙親委派規則,其餘的類查找都是在平級的類加載器中進行的。

      只要有足夠意義和理由,突破已有的原則就可認爲是一種創新。正如OSGi中的類加載器並不符合傳統的雙親委派的類加載器,並且業界對其爲了實現熱部署而帶來的額外的高複雜度還存在不少爭議,但在Java程序員中基本有一個共識:OSGi中對類加載器的使用是很值得學習的,弄懂了OSGi的實現,就可以算是掌握了類加載器的精髓。

 

Tomcat的類加載器架構

 

   

      主流的Java Web服務器(也就是Web容器),如Tomcat、Jetty、WebLogic、WebSphere或其他筆者沒有列舉的服務器,都實現了自己定義的類加載器(一般都不止一個)。因爲一個功能健全的Web容器,要解決如下幾個問題:

      1)部署在同一個Web容器上兩個Web應用程序使用的Java類庫可以實現相互隔離。這是最基本的需求,兩個不同的應用程序可能會依賴同一個第三方類庫的不同版本,不能要求一個類庫在一個服務器中只有一份,服務器應當保證兩個應用程序的類庫可以互相獨立使用。

      2)部署在同一個Web容器上兩個Web應用程序所使用的Java類庫可以互相共享。這個需求也很常見,例如,用戶可能有10個使用Spring組織的應用程序部署在同一臺服務器上,如果把10份Spring分別存放在各個應用程序的隔離目錄中,將會是很大的資源浪費——這主要倒不是浪費磁盤空間的問題,而是指類庫在使用時都要被加載到Web容器的內存,如果類庫不能共享,虛擬機的方法區就會很容易出現過度膨脹的風險

      3)Web容器需要儘可能地保證自身的安全不受部署的Web應用程序影響。目前,有許多主流的Java Web容器自身也是使用Java語言來實現的。因此,Web容器本身也有類庫依賴的問題,一般來說,基於安全考慮,容器所使用的類庫應該與應用程序的類庫互相獨立。

      4)支持JSP應用的Web容器,大多數都需要支持HotSwap功能。我們知道,JSP文件最終要編譯成Java Class才能由虛擬機執行,但JSP文件由於其純文本存儲的特性,運行時修改的概率遠遠大於第三方類庫或程序自身的Class文件。而且ASP、PHP和JSP這些網頁應用也把修改後無須重啓作爲一個很大的“優勢”來看待,因此“主流”的Web容器都會支持JSP生成類的熱替換,當然也有“非主流”的,如運行在生產模式(Production Mode)下的WebLogic服務器默認就不會處理JSP文件的變化。

      由於存在上述問題,在部署Web應用時,單獨的一個Class Path就無法滿足需求了,所以各種Web容都“不約而同”地提供了好幾個Class Path路徑供用戶存放第三方類庫,這些路徑一般都以“lib”或“classes”命名。被放置到不同路徑中的類庫,具備不同的訪問範圍和服務對象,通常,每一個目錄都會有一個相應的自定義類加載器去加載放置在裏面的Java類庫。現在,就以Tomcat容器爲例,看一看Tomcat具體是如何規劃用戶類庫結構和類加載器的。

      在Tomcat目錄結構中,有3組目錄(“/common/*”、“/server/*”和“/shared/*”)可以存放Java類庫,另外還可以加上Web應用程序自身的目錄“/WEB-INF/*”,一共4組,把Java類庫放置在這些目錄中的含義分別如下:

      ①放置在/common目錄中:類庫可被Tomcat和所有的Web應用程序共同使用

      ②放置在/server目錄中:類庫可被Tomcat使用,對所有的Web應用程序都不可見。

      ③放置在/shared目錄中:類庫可被所有的Web應用程序共同使用,但對Tomcat自己不可見。

      ④放置在/WebApp/WEB-INF目錄中:類庫僅僅可以被此Web應用程序使用,對Tomcat和其他Web應用程序都不可見。

      爲了支持這套目錄結構,並對目錄裏面的類庫進行加載和隔離,Tomcat自定義了多個類加載器,這些類加載器按照經典的雙親委派模型來實現,其關係如下圖所示。

 

 

 

 

      上圖中灰色背景的3個類加載器是JDK默認提供的類加載器,這3個加載器的作用已經介紹過了。而CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader則是Tomcat自己定義的類加載器,它們分別加載/common/*、/server/*、/shared/*和/WebApp/WEB-INF/*中的Java類庫。其中WebApp類加載器和Jsp類加載器通常會存在多個實例每一個Web應用程序對應一個WebApp類加載器每一個JSP文件對應一個Jsp類加載器

   從圖中的委派關係中可以看出,CommonClassLoader能加載的類都可以被Catalina ClassLoader和SharedClassLoader使用,而CatalinaClassLoader和Shared ClassLoader自己能加載的類則與對方相互隔離。WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個WebAppClassLoader實例之間相互隔離。而JasperLoader的加載範圍僅僅是這個JSP文件所編譯出來的那一個.Class文件,它出現的目的就是爲了被丟棄:當Web容器檢測到JSP文件被修改時,會替換掉目前的JasperLoader的實例,並通過再建立一個新的Jsp類加載器來實現JSP文件的HotSwap功能。

      對於Tomcat的6.x版本,只有指定了tomcat/conf/catalina.properties配置文件的server.loader和share.loader項後纔會真正建立Catalina ClassLoader和Shared ClassLoader的實例,否則在用到這兩個類加載器的地方都會用Common ClassLoader的實例代替,而默認的配置文件中沒有設置這兩個loader項,所以Tomcat 6.x順理成章地把/common、/server和/shared三個目錄默認合併到一起變成一個/lib目錄,這個目錄裏的類庫相當於以前/common目錄中類庫的作用。這是Tomcat設計團隊爲了簡化大多數的部署場景所做的一項改進,如果默認設置不能滿足需要,用戶可以通過修改配置文件指定server.loader和share.loader的方式重新啓用Tomcat 5.x的加載器架構。

      Tomcat加載器的實現清晰易懂,並且採用了官方推薦的“正統”的使用類加載器的方式。如果讀者閱讀完上面的案例後,能完全理解Tomcat設計團隊這樣佈置加載器架構的用意,那說明已經大致掌握了類加載器“主流”的使用方式,那麼筆者不妨再提一個問題讓讀者思考一下:前面曾經提到過一個場景,如果有10個Web應用程序都是用Spring來進行組織和管理的話,可以把Spring放到Common或Shared目錄下讓這些程序共享。Spring要對用戶程序的類進行管理,自然要能訪問到用戶程序的類,而用戶的程序顯然是放在/WebApp/WEB-INF目錄中的,那麼被CommonClassLoader或SharedClassLoader加載的Spring如何訪問並不在其加載範圍內的用戶程序呢?如果研究過虛擬機類加載器機制中的雙親委派模型,相信讀者可以很容易地回答這個問題。

      分析:如果按主流的雙親委派機制,顯然無法做到讓父類加載器加載的類去訪問子類加載器加載的類,上面在類加載器一節中提到過通過線程上下文方式傳播類加載器。

      答案是使用線程上下文類加載器來實現的,使用線程上下文加載器,可以讓父類加載器請求子類加載器去完成類加載的動作。看spring源碼發現,spring加載類所用的Classloader是通過Thread.currentThread().getContextClassLoader()來獲取的,而當線程創建時會默認setContextClassLoader(AppClassLoader),即線程上下文類加載器被設置爲AppClassLoader,spring中始終可以獲取到這個AppClassLoader(在Tomcat裏就是WebAppClassLoader)子類加載器來加載bean,以後任何一個線程都可以通過getContextClassLoader()獲取到WebAppClassLoader來getbean了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章