Java類載入機制與Tomcat類載入器架構__Java

來源:互聯網
上載者:User
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()方法之中,如以下代碼所示,邏輯清晰易懂:

protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{//首先, 檢查請求的類是否已經被載入過了Class c=findLoadedClass(name);if( c== null ){try{if( parent != null ){c = parent.loadClass(name,false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {//如果父類載入器拋出ClassNotFoundException//說明父類載入器無法完成載入請求}if( c == null ) {//在父類載入器無法載入的時候//再調用本身的findClass方法來進行類載入c = findClass(name);}} if(resolve){resolveClass(c);}return c;}

      先檢查是否已經被載入過,若沒有載入則調用父載入器的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)等,說白了就是希望應用程式能像我們的電腦外設那樣,接上滑鼠、隨身碟,不用重啟機器就能立即使用,滑鼠有問題或要升級就換個滑鼠,不用停機也不用重啟。對於個人電腦來說,重啟一次其實沒有什麼大不了的,但對於一些生產系統來說,關機重啟一次可能就要被列為生產事故,這種情況下熱部署就對軟體開發人員,尤其是企業級軟體開發人員具有很大的吸引力。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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.