您好,登錄后才能下訂單哦!
虛擬機設計團隊把類加載階段中的“通過一個類的全限定名來獲取描述此類的二進制字節流(即字節碼)”這個動作放到Java虛擬機外部去實現,以便讓應用程序自己決定如何去獲取所需要的類。實現這個動作的代碼模塊稱為“類加載器”。
一般來說,Java 虛擬機使用 Java 類的方式如下:
Java 源程序(.java 文件)在經過 Java 編譯器編譯之后就被轉換成字節碼(.class 文件)。
類加載器負責讀取 Java 字節代碼,并轉換成?java.lang.Class
類的一個實例。每個這樣的實例用來表示一個 Java 類。通過此實例的?newInstance()
方法就可以創建出該類的一個對象。
實際的情況可能更加復雜,比如 Java 字節代碼可能是通過工具動態生成的,也可能是通過網絡下載的。更詳細的內容可以參考上一篇文章中講類加載過程中的加載階段時介紹的幾個例子(JAR包、Applet、動態代理、JSP等)。
類加載器雖然只用于實現類的加載動作,但它在Java程序起到的作用卻遠大于類加載階段。對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。通俗而言:比較兩個類是否“相等”(這里所指的“相等”,包括類的Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果,也包括使用instanceof()關鍵字對做對象所屬關系判定等情況),只有在這兩個類時由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。
從jvm的角度來講,只存在以下兩種不同的類加載器:
啟動類加載器(Bootstrap ClassLoader),這個類加載器用C++實現,是虛擬機自身的一部分;
所有其他類的加載器,這些類由Java實現,獨立于虛擬機外部,并且全都繼承自抽象類java.lang.ClassLoader
。
從Java開發人員的角度看,類加載器可以劃分得更細致一些:
啟動類加載器(Bootstrap ClassLoader)?此類加載器負責將存放在?<JAVA_HOME>\lib
?目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中的,并且是虛擬機識別的(僅按照文件名識別,如 rt.jar,名字不符合的類庫即使放在lib 目錄中也不會被加載)類庫加載到虛擬機內存中。
啟動類加載器無法被 Java 程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給引導類加載器,直接使用null代替即可。
擴展類加載器(Extension ClassLoader)?這個類加載器是由ExtClassLoader(sun.misc.Launcher$ExtClassLoader)
實現的。它負責將<Java_Home>/lib/ext
或者被?java.ext.dir
系統變量所指定路徑中的所有類庫加載到內存中,開發者可以直接使用擴展類加載器。
應用程序類加載器(Application ClassLoader)?這個類加載器是由?AppClassLoader(sun.misc.Launcher$AppClassLoader)
實現的。由于這個類加載器是ClassLoader
中的getSystemClassLoader()
方法的返回值,因此一般稱為系統類加載器。
它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
由開發人員開發的應用程序都是由這三種類加載器相互配合進行加載的,如果有必要,還可以加入自己定義的類加載器。這些類加載器的關系一般如下圖所示:
上圖展示的類加載器之間的層次關系,稱為類加載器的雙親委派模型(Parents Delegation Model)。該模型要求除了頂層的啟動類加載器外,其余的類加載器都應有自己的父類加載器,這里類加載器之間的父子關系一般通過組合(Composition)關系來實現,而不是通過繼承(Inheritance)的關系實現。
工作過程
如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載,而是把這個請求委派給父類加載器,每一個層次的加載器都是如此,依次遞歸,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父加載器反饋自己無法完成此加載請求(它搜索范圍中沒有找到所需類)時,子加載器才會嘗試自己加載。
優點
使用雙親委派模型來組織類加載器之間的關系,使得Java類隨著它的類加載器一起具備了一種帶有優先級的層次關系。例如類java.lang.Object
,它存放再rt.jar
中,無論哪個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。
相反,如果沒有雙親委派模型,由各個類加載器自行加載的話,如果用戶編寫了一個稱為`java.lang.Object
的類,并放在程序的ClassPath中,那系統中將會出現多個不同的Object類,程序將變得一片混亂。如果開發者嘗試編寫一個與rt.jar
類庫中已有類重名的Java類,將會發現可以正常編譯,但是永遠無法被加載運行。
雙親委派模型的實現如下:
protected?synchronized?Class<?>?loadClass(String?name,boolean?resolve)throws?ClassNotFoundException{ ????//check?the?class?has?been?loaded?or?not????Class?c?=?findLoadedClass(name); ????if(c?==?null){ ????????try{ ????????????if(parent?!=?null){ ????????????????c?=?parent.loadClass(name,false); ????????????}else{ ????????????????c?=?findBootstrapClassOrNull(name); ????????????} ????????}catch(ClassNotFoundException?e){ ????????????//if?throws?the?exception?,the?father?can?not?complete?the?load????????} ????????if(c?==?null){ ????????????c?=?findClass(name); ????????} ????} ????if(resolve){ ????????resolveClass(c); ????} ????return?c;}
雙親委派模型并不能解決 Java 應用開發中會遇到的類加載器的全部問題。Java 提供了很多服務提供者接口(Service Provider Interface,SPI),允許第三方為這些接口提供實現。常見的 SPI 有?JDBC、JCE、JNDI、JAXP 和 JBI?等。這些?SPI 的接口由 Java 核心庫來提供,如 JAXP 的 SPI 接口定義包含在?javax.xml.parsers
包中。
這些 SPI 的實現代碼很可能是作為 Java 應用所依賴的?jar 包被包含進來,可以通過類路徑(ClassPath)來找到,如實現了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 接口中的代碼經常需要加載具體的實現類。如 JAXP 中的?javax.xml.parsers.DocumentBuilderFactory
類中的?newInstance()
?方法用來生成一個新的?DocumentBuilderFactory
?的實例。
這里的實例的真正的類是繼承自?javax.xml.parsers.DocumentBuilderFactory
,由 SPI 的實現所提供的。如在 Apache Xerces 中,實現的類是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。
而問題在于,SPI 的接口是Java 核心庫的一部分,是由引導類加載器加載的,而SPI 實現的 Java 類一般是由系統類加載器加載的。引導類加載器是無法找到 SPI 的實現類的,因為它只加載 Java 的核心庫。
它也不能委派給系統類加載器,因為它是系統類加載器的祖先類加載器。也就是說,類加載器的雙親委派模型無法解決這個問題。
為了解決這個問題,Java設計團隊只好引入了一個不太優雅的設計:線程上下文類加載器(Thread Context ClassLoader)。
線程上下文類加載器是從 JDK 1.2 開始引入的。類?java.lang.Thread
中的方法?getContextClassLoader()
和?setContextClassLoader(ClassLoader cl)
用來獲取和設置線程的上下文類加載器。
如果沒有通過?setContextClassLoader(ClassLoader cl)
方法進行設置的話,線程將繼承其父線程的上下文類加載器。Java 應用運行的初始線程的上下文類加載器是應用程序類加載器。在線程中運行的代碼可以通過此類加載器來加載類和資源。
有了線程上下文類加載器,就可以做一些“舞弊”的事情了,JNDI服務使用這個線程上下文類加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載器的動作,這種行為實際上就是打通了雙親委派模型的層次結構來逆向使用類加載器,已經違背了雙親委派模型的一般性原則。
這里所說的“動態性”指的是當前一些非常熱門的名詞:代碼熱替換(HotSwap)、模塊熱部署(Hot Deployment)等。即希望應用程序能像計算機的外設一樣,接上鼠標、鍵盤,不用重啟就能立即使用,鼠標出了問題或需要升級就換個鼠標,不用停機或重啟。
當前業界“事實上”的Java模塊化標準是OSGi,而OSGi實現代碼熱部署的關鍵則是它自定義的類機載器的實現。關于OSGi的細節將在稍后的案例分析中詳細講解。
API
其中有如下三個比較重要的方法
在了解完上述內容后,我們可以容易地意識到自定義類加載器有以下兩種方式:
采用雙親委派模型:繼承ClassLoader類,只需重寫其的findClass(String name)
方法,而不需重寫loadClass(String name)
方法。
破壞雙親委派模型:繼承ClassLoader類,需要整個重寫實現了雙親委派模型邏輯的loadClass(String name)
方法。
下面我們來實現一個自定義類加載器,用來加載存儲在文件系統上的 Java 字節代碼。
public?class?FileSystemClassLoader?extends?ClassLoader?{? ? ???private?String?rootDir;? ? ???public?FileSystemClassLoader(String?rootDir)?{? ???????this.rootDir?=?rootDir;? ???}? ? ???@Override ???protected?Class<?>?findClass(String?name)?throws?ClassNotFoundException?{? ???????byte[]?classData?=?getClassData(name);? ???????if?(classData?==?null)?{? ???????????throw?new?ClassNotFoundException();? ???????}? ???????else?{? ???????????return?defineClass(name,?classData,?0,?classData.length);? ???????}? ???}? ? ???private?byte[]?getClassData(String?className)?{? ???????String?path?=?classNameToPath(className);? ???????try?{? ???????????InputStream?ins?=?new?FileInputStream(path);? ???????????ByteArrayOutputStream?baos?=?new?ByteArrayOutputStream();? ???????????int?bufferSize?=?4096;? ???????????byte[]?buffer?=?new?byte[bufferSize];? ???????????int?bytesNumRead?=?0;? ???????????while?((bytesNumRead?=?ins.read(buffer))?!=?-1)?{? ???????????????baos.write(buffer,?0,?bytesNumRead);? ???????????}? ???????????return?baos.toByteArray();? ???????}?catch?(IOException?e)?{? ???????????e.printStackTrace();? ???????}? ???????return?null;? ???}? ? ???private?String?classNameToPath(String?className)?{? ???????return?rootDir?+?File.separatorChar? ???????????????+?className.replace('.',?File.separatorChar)?+?".class";? ???}?}
類 FileSystemClassLoader的?findClass()
方法首先根據類的全名在硬盤上查找類的字節代碼文件(.class 文件),然后讀取該文件內容,最后通過 defineClass()方法來把這些字節代碼轉換成?java.lang.Class
類的實例。
主流的Java Web服務器如Tomcat、Jetty、WebLogic、WebSphere等等,都實現了自己定義的類加載器(一般都不止一個)。因為一個功能健全的Web服務器,要解決以下問題:
部署在同一個服務器上的兩個Web應用程序所使用的Java類庫可以實現相互隔離。?兩個不同的應用程序可能會依賴同一個第三方類庫的不同版本,不能要求一個類庫在一個服務器中只有一份,服務器應當保證兩個應用程序的類庫可以互相獨立使用。
部署在同一個服務器上的兩個Web應用程序所使用的Java類庫可以相互共享。?例如,用戶可能有5個使用Spring組織的應用程序部署在同一臺服務器上,如果把5份Spring分別放在各個應用程序的隔離目錄中,庫在使用時都要被加載到服務器內存中,JVM的方法區就會有過度膨脹的風險。
服務器需要盡可能保證自身安全不受部署的Web應用程序影響。?很多Web服務器本身是用Java實現的,服務器使用的類庫應該與應用程序的類庫相互獨立。
支持JSP應用的服務器,大多數需要支持代碼熱替換(HotSwap)功能。?JSP文件由于其純文本存儲的特性,運行時修改的概率遠大于第三方類庫或程序自身的Class文件,因此需要做到修改后無須重啟。
鑒于上述問題,各種Web服務器都不約而同地提供了數個ClassPath路徑供用戶存放第三方類庫,這些路徑一般以“lib”或“classes”命名。以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采用如下經典的雙親委派模型來實現了多個類加載器:
CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader是Tomcat自己定義的類加載器,它們分別加載/common/* 、/server/*、/shared/**和/WebApp/WEB-INF/*中的Java類庫。其中WebApp類加載器和JSP類加載器通常會存在多個實例,每一個Web應用程序對應一個WebApp類加載器,每一個JSP文件對應一個JSP類加載器。
CommonClassLoader能加載的類都可以被CatalinaClassLoader和SharedClassLoader使用,而CatalinaClassLoader和SharedClassLoader自己能加載的類則與對方相互隔離。WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個WebAppClassLoader實例之間相互隔離。而JasperLoader的加載范圍僅是這個JSP文件編譯出來的那一個Class,它出現的目的就是被丟棄。當服務器檢測到JSP文件被修改時,會替換掉目前的JasperLoader的實例,并通過再建立一個新的JSP類加載器來實現JSP文件的HotSwap功能。
特殊場景
前文提到過一個場景,如果有5個Web應用程序都是用Spring來進行組織和管理的話,可以把Spring放到Common或Shared目錄下讓這些程序共享。Spring要對用戶程序的類進行管理,自然要能訪問到用戶程序的類,而用戶程序放在/WebApp/WEB-INF目錄中,這時就需要破壞雙親委派模型,使用線程上下文類加載器來完成這一工作了。
OSGi(Open Service Gateway Initiative)是OSGi聯盟制定的一個基于Java語言的動態模塊化規范,現在成為了Java“事實上”的模塊化標準。它為開發人員提供了面向服務和基于組件的運行環境,并提供標準的方式用來管理軟件的生命周期。OSGi 已經被實現和部署在很多產品上,在開源社區也得到了廣泛的支持,其中最為著名的應用莫過于大家都很熟悉的Eclipse IDE。
OSGi 中的每個模塊(bundle)都包含?Java Package和Class。模塊可以聲明它所依賴的需要導入(import)的其它模塊的 Java 包和類(通過?Import-Package),也可以聲明導出(export)自己的包和類,供其它模塊使用(通過?Export-Package)。也就是說需要能夠隱藏和共享一個模塊中的某些 Java 包和類。這是通過 OSGi 特有的類加載器機制來實現的。
OSGi 中的每個模塊都有對應的一個類加載器,它負責加載模塊自己包含的 Java 包和類。當它需要加載 Java 核心庫的類時(以 java開頭的包和類),它會代理給父類加載器(通常是啟動類加載器)來完成。當它需要加載所導入的 Java 類時,它會代理給導出此 Java 類的模塊來完成加載。模塊也可以顯式的聲明某些 Java 包和類,必須由父類加載器來加載。只需要設置系統屬性?org.osgi.framework.bootdelegation
的值即可。
假設有兩個模塊 bundleA 和 bundleB,它們都有自己對應的類加載器 ClassLoaderA 和 ClassLoaderB。在 bundleA 中包含類 com.bundleA.Sample,并且該類被聲明為導出的,也就是說可以被其它模塊所使用的。
bundleB 聲明了導入 bundleA 提供的類?com.bundleA.Sample
,并包含一個類?com.bundleB.NewSample
繼承自?com.bundleA.Sample
。在 bundleB 啟動的時候,其類加載器 classLoaderB 需要加載類?com.bundleB.NewSample
,進而需要加載類?com.bundleA.Sample
。
由于 bundleB 聲明了類?com.bundleA.Sample
是導入的,classLoaderB 把加載類?com.bundleA.Sample
的工作代理給導出該類的 bundleA 的類加載器 ClassLoaderA。ClassLoaderA 在其模塊內部查找類?com.bundleA.Sample
并定義它,所得到的類?com.bundleA.Sample
實例就可以被所有聲明導入了此類的模塊使用。
對于以 java開頭的類,都是由父類加載器來加載的。
如果聲明了系統屬性?org.osgi.framework.bootdelegation=com.example.core.*
,那么對于包?com.example.core
中的類,都是由父類加載器來完成的。
OSGi 模塊的這種類加載器結構,使得一個類的不同版本可以共存在 Java 虛擬機中,帶來了很大的靈活性。不過它的這種不同,也會給開發人員帶來一些麻煩,尤其當模塊需要使用第三方提供的庫的時候。下面提供幾條比較好的建議:
如果一個類庫只有一個模塊使用,把該類庫的 jar 包放在模塊中,在 Bundle-ClassPath中指明即可。
如果一個類庫被多個模塊共用,可以為這個類庫單獨的創建一個模塊,把其它模塊需要用到的 Java 包聲明為導出的。其它模塊聲明導入這些類。
如果類庫提供了 SPI 接口,并且利用線程上下文類加載器來加載 SPI 實現的 Java 類,有可能會找不到 Java 類。如果出現了 NoClassDefFoundError異常,首先檢查當前線程的上下文類加載器是否正確。通過?Thread.currentThread().getContextClassLoader()
就可以得到該類加載器。該類加載器應該是該模塊對應的類加載器。如果不是的話,可以首先通過?class.getClassLoader()
來得到模塊對應的類加載器,再通過?Thread.currentThread().setContextClassLoader()
來設置當前線程的上下文類加載器。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。