深入理解JVM(6)——类加载器
虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)把類加載階段中的“通過一個(gè)類的全限定名來獲取描述此類的二進(jìn)制字節(jié)流(即字節(jié)碼)”這個(gè)動作放到Java虛擬機(jī)外部去實(shí)現(xiàn),以便讓應(yīng)用程序自己決定如何去獲取所需要的類。實(shí)現(xiàn)這個(gè)動作的代碼模塊稱為“類加載器”。
一般來說,Java 虛擬機(jī)使用 Java 類的方式如下:
實(shí)際的情況可能更加復(fù)雜,比如 Java 字節(jié)代碼可能是通過工具動態(tài)生成的,也可能是通過網(wǎng)絡(luò)下載的。更詳細(xì)的內(nèi)容可以參考上一篇文章中講類加載過程中的加載階段時(shí)介紹的幾個(gè)例子(JAR包、Applet、動態(tài)代理、JSP等)。
類與類加載器
類加載器雖然只用于實(shí)現(xiàn)類的加載動作,但它在Java程序起到的作用卻遠(yuǎn)大于類加載階段。對于任意一個(gè)類,都需要由加載它的類加載器和這個(gè)類本身一同確立其在Java虛擬機(jī)中的唯一性,每一個(gè)類加載器,都擁有一個(gè)獨(dú)立的類名稱空間。通俗而言:比較兩個(gè)類是否“相等”(這里所指的“相等”,包括類的Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結(jié)果,也包括使用instanceof()關(guān)鍵字對做對象所屬關(guān)系判定等情況),只有在這兩個(gè)類時(shí)由同一個(gè)類加載器加載的前提下才有意義,否則,即使這兩個(gè)類來源于同一個(gè)Class文件,被同一個(gè)虛擬機(jī)加載,只要加載它們的類加載器不同,那這兩個(gè)類就必定不相等。
雙親委派模型
從jvm的角度來講,只存在以下兩種不同的類加載器:
- 啟動類加載器(Bootstrap ClassLoader),這個(gè)類加載器用C++實(shí)現(xiàn),是虛擬機(jī)自身的一部分;
- 所有其他類的加載器,這些類由Java實(shí)現(xiàn),獨(dú)立于虛擬機(jī)外部,并且全都繼承自抽象類java.lang.ClassLoader。
從Java開發(fā)人員的角度看,類加載器可以劃分得更細(xì)致一些:
- 啟動類加載器(Bootstrap ClassLoader)?此類加載器負(fù)責(zé)將存放在?<JAVA_HOME>\lib?目錄中的,或者被 -Xbootclasspath 參數(shù)所指定的路徑中的,并且是虛擬機(jī)識別的(僅按照文件名識別,如 rt.jar,名字不符合的類庫即使放在lib 目錄中也不會被加載)類庫加載到虛擬機(jī)內(nèi)存中。 啟動類加載器無法被 Java 程序直接引用,用戶在編寫自定義類加載器時(shí),如果需要把加載請求委派給引導(dǎo)類加載器,直接使用null代替即可。
- 擴(kuò)展類加載器(Extension ClassLoader)?這個(gè)類加載器是由ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實(shí)現(xiàn)的。它負(fù)責(zé)將<Java_Home>/lib/ext或者被?java.ext.dir系統(tǒng)變量所指定路徑中的所有類庫加載到內(nèi)存中,開發(fā)者可以直接使用擴(kuò)展類加載器。
- 應(yīng)用程序類加載器(Application ClassLoader)?這個(gè)類加載器是由?AppClassLoader(sun.misc.Launcher$AppClassLoader)實(shí)現(xiàn)的。由于這個(gè)類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,因此一般稱為系統(tǒng)類加載器。它負(fù)責(zé)加載用戶類路徑(ClassPath)上所指定的類庫,開發(fā)者可以直接使用這個(gè)類加載器,如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個(gè)就是程序中默認(rèn)的類加載器。
由開發(fā)人員開發(fā)的應(yīng)用程序都是由這三種類加載器相互配合進(jìn)行加載的,如果有必要,還可以加入自己定義的類加載器。這些類加載器的關(guān)系一般如下圖所示:
上圖展示的類加載器之間的層次關(guān)系,稱為類加載器的雙親委派模型(Parents Delegation Model)。該模型要求除了頂層的啟動類加載器外,其余的類加載器都應(yīng)有自己的父類加載器,這里類加載器之間的父子關(guān)系一般通過組合(Composition)關(guān)系來實(shí)現(xiàn),而不是通過繼承(Inheritance)的關(guān)系實(shí)現(xiàn)。
工作過程
如果一個(gè)類加載器收到了類加載的請求,它首先不會自己去嘗試加載,而是把這個(gè)請求委派給父類加載器,每一個(gè)層次的加載器都是如此,依次遞歸,因此所有的加載請求最終都應(yīng)該傳送到頂層的啟動類加載器中,只有當(dāng)父加載器反饋?zhàn)约簾o法完成此加載請求(它搜索范圍中沒有找到所需類)時(shí),子加載器才會嘗試自己加載。
優(yōu)點(diǎn)
使用雙親委派模型來組織類加載器之間的關(guān)系,使得Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系。例如類java.lang.Object,它存放再rt.jar中,無論哪個(gè)類加載器要加載這個(gè)類,最終都是委派給處于模型最頂端的啟動類加載器進(jìn)行加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個(gè)類。
相反,如果沒有雙親委派模型,由各個(gè)類加載器自行加載的話,如果用戶編寫了一個(gè)稱為`java.lang.Object的類,并放在程序的ClassPath中,那系統(tǒng)中將會出現(xiàn)多個(gè)不同的Object類,程序?qū)⒆兊靡黄靵y。如果開發(fā)者嘗試編寫一個(gè)與rt.jar類庫中已有類重名的Java類,將會發(fā)現(xiàn)可以正常編譯,但是永遠(yuǎn)無法被加載運(yùn)行。
雙親委派模型的實(shí)現(xiàn)如下:
protected synchronized Class<?> loadClass(String name,boolean resolve)throws ClassNotFoundException{//check the class has been loaded or notClass 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 應(yīng)用開發(fā)中會遇到的類加載器的全部問題。Java 提供了很多服務(wù)提供者接口(Service Provider Interface,SPI),允許第三方為這些接口提供實(shí)現(xiàn)。常見的 SPI 有?JDBC、JCE、JNDI、JAXP 和 JBI?等。這些?SPI 的接口由 Java 核心庫來提供,如 JAXP 的 SPI 接口定義包含在?javax.xml.parsers包中。這些 SPI 的實(shí)現(xiàn)代碼很可能是作為 Java 應(yīng)用所依賴的?jar 包被包含進(jìn)來,可以通過類路徑(ClassPath)來找到,如實(shí)現(xiàn)了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 接口中的代碼經(jīng)常需要加載具體的實(shí)現(xiàn)類。如 JAXP 中的?javax.xml.parsers.DocumentBuilderFactory類中的?newInstance()?方法用來生成一個(gè)新的?DocumentBuilderFactory?的實(shí)例。這里的實(shí)例的真正的類是繼承自?javax.xml.parsers.DocumentBuilderFactory,由 SPI 的實(shí)現(xiàn)所提供的。如在 Apache Xerces 中,實(shí)現(xiàn)的類是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而問題在于,SPI 的接口是Java 核心庫的一部分,是由引導(dǎo)類加載器加載的,而SPI 實(shí)現(xiàn)的 Java 類一般是由系統(tǒng)類加載器加載的。引導(dǎo)類加載器是無法找到 SPI 的實(shí)現(xiàn)類的,因?yàn)樗患虞d Java 的核心庫。它也不能委派給系統(tǒng)類加載器,因?yàn)樗窍到y(tǒng)類加載器的祖先類加載器。也就是說,類加載器的雙親委派模型無法解決這個(gè)問題。
為了解決這個(gè)問題,Java設(shè)計(jì)團(tuán)隊(duì)只好引入了一個(gè)不太優(yōu)雅的設(shè)計(jì):線程上下文類加載器(Thread Context ClassLoader)。線程上下文類加載器是從 JDK 1.2 開始引入的。類?java.lang.Thread中的方法?getContextClassLoader()和?setContextClassLoader(ClassLoader cl)用來獲取和設(shè)置線程的上下文類加載器。如果沒有通過?setContextClassLoader(ClassLoader cl)方法進(jìn)行設(shè)置的話,線程將繼承其父線程的上下文類加載器。Java 應(yīng)用運(yùn)行的初始線程的上下文類加載器是應(yīng)用程序類加載器。在線程中運(yùn)行的代碼可以通過此類加載器來加載類和資源。
有了線程上下文類加載器,就可以做一些“舞弊”的事情了,JNDI服務(wù)使用這個(gè)線程上下文類加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載器的動作,這種行為實(shí)際上就是打通了雙親委派模型的層次結(jié)構(gòu)來逆向使用類加載器,已經(jīng)違背了雙親委派模型的一般性原則。
追求程序動態(tài)性
這里所說的“動態(tài)性”指的是當(dāng)前一些非常熱門的名詞:代碼熱替換(HotSwap)、模塊熱部署(Hot Deployment)等。即希望應(yīng)用程序能像計(jì)算機(jī)的外設(shè)一樣,接上鼠標(biāo)、鍵盤,不用重啟就能立即使用,鼠標(biāo)出了問題或需要升級就換個(gè)鼠標(biāo),不用停機(jī)或重啟。
當(dāng)前業(yè)界“事實(shí)上”的Java模塊化標(biāo)準(zhǔn)是OSGi,而OSGi實(shí)現(xiàn)代碼熱部署的關(guān)鍵則是它自定義的類機(jī)載器的實(shí)現(xiàn)。關(guān)于OSGi的細(xì)節(jié)將在稍后的案例分析中詳細(xì)講解。
自定義類加載器
API
其中有如下三個(gè)比較重要的方法
| defineClass(String name, byte[] b, int off, int len) | 把字節(jié)數(shù)組 b中的內(nèi)容轉(zhuǎn)換成 Java 類,該字節(jié)數(shù)組可以看成是二進(jìn)制流字節(jié)組成的文件,返回的結(jié)果是java.lang.Class類的實(shí)例。這個(gè)方法被聲明為 final的。 |
| loadClass(String name) | 上文中已貼出源碼,實(shí)現(xiàn)了雙親委派模型,調(diào)用findClass()執(zhí)行類加載動作,返回的是java.lang.Class類的實(shí)例。 |
| findClass(String name) | 通過傳入的類全限定名name來獲取對應(yīng)的類,返回的是java.lang.Class類的實(shí)例,該類沒有提供具體的實(shí)現(xiàn),開發(fā)者在自定義類加載器時(shí)需重用此方法,在實(shí)現(xiàn)此方法時(shí)需調(diào)用defineClass(String name, byte[] b, int off, int len)方法。 |
在了解完上述內(nèi)容后,我們可以容易地意識到自定義類加載器有以下兩種方式:
- 采用雙親委派模型:繼承ClassLoader類,只需重寫其的findClass(String name)方法,而不需重寫loadClass(String name)方法。
- 破壞雙親委派模型:繼承ClassLoader類,需要整個(gè)重寫實(shí)現(xiàn)了雙親委派模型邏輯的loadClass(String name)方法。
實(shí)例
下面我們來實(shí)現(xiàn)一個(gè)自定義類加載器,用來加載存儲在文件系統(tǒng)上的 Java 字節(jié)代碼。
public class FileSystemClassLoader extends ClassLoader { private String rootDir; public FileSystemClassLoader(String rootDir) { this.rootDir = rootDir; } @Overrideprotected 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()方法首先根據(jù)類的全名在硬盤上查找類的字節(jié)代碼文件(.class 文件),然后讀取該文件內(nèi)容,最后通過 defineClass()方法來把這些字節(jié)代碼轉(zhuǎn)換成?java.lang.Class類的實(shí)例。
案例分析
Tomcat:正統(tǒng)的類加載器架構(gòu)
主流的Java Web服務(wù)器如Tomcat、Jetty、WebLogic、WebSphere等等,都實(shí)現(xiàn)了自己定義的類加載器(一般都不止一個(gè))。因?yàn)橐粋€(gè)功能健全的Web服務(wù)器,要解決以下問題:
- 部署在同一個(gè)服務(wù)器上的兩個(gè)Web應(yīng)用程序所使用的Java類庫可以實(shí)現(xiàn)相互隔離。?兩個(gè)不同的應(yīng)用程序可能會依賴同一個(gè)第三方類庫的不同版本,不能要求一個(gè)類庫在一個(gè)服務(wù)器中只有一份,服務(wù)器應(yīng)當(dāng)保證兩個(gè)應(yīng)用程序的類庫可以互相獨(dú)立使用。
- 部署在同一個(gè)服務(wù)器上的兩個(gè)Web應(yīng)用程序所使用的Java類庫可以相互共享。?例如,用戶可能有5個(gè)使用Spring組織的應(yīng)用程序部署在同一臺服務(wù)器上,如果把5份Spring分別放在各個(gè)應(yīng)用程序的隔離目錄中,庫在使用時(shí)都要被加載到服務(wù)器內(nèi)存中,JVM的方法區(qū)就會有過度膨脹的風(fēng)險(xiǎn)。
- 服務(wù)器需要盡可能保證自身安全不受部署的Web應(yīng)用程序影響。?很多Web服務(wù)器本身是用Java實(shí)現(xiàn)的,服務(wù)器使用的類庫應(yīng)該與應(yīng)用程序的類庫相互獨(dú)立。
- 支持JSP應(yīng)用的服務(wù)器,大多數(shù)需要支持代碼熱替換(HotSwap)功能。?JSP文件由于其純文本存儲的特性,運(yùn)行時(shí)修改的概率遠(yuǎn)大于第三方類庫或程序自身的Class文件,因此需要做到修改后無須重啟。
鑒于上述問題,各種Web服務(wù)器都不約而同地提供了數(shù)個(gè)ClassPath路徑供用戶存放第三方類庫,這些路徑一般以“l(fā)ib”或“classes”命名。以Tomcat為例,有3組目錄(“/common/* ”、“/server/* ”和“/shared/* ”)可以存放Java類庫,另外還可以加上Web應(yīng)用程序自身的目錄“/WEB-INF/* ”,一共4組,把Java類庫放置在這些目錄中的含義分別如下:
- /common目錄:類庫可被Tomcat和所有的Web應(yīng)用程序共同使用。
- /server目錄:類庫可被Tomcat使用,對所有的Web應(yīng)用程序都不可見。
- /shared目錄:類庫可被所有的Web應(yīng)用程序共同使用,但對Tomcat自己不可見。
- /WebApp/WEB-INF目錄:類庫僅僅可以被此Web應(yīng)用程序使用,對Tomcat和其他Web應(yīng)用程序都不可見。
為了支持這套目錄結(jié)構(gòu),并對目錄里的類庫進(jìn)行加載和隔離,Tomcat采用如下經(jīng)典的雙親委派模型來實(shí)現(xiàn)了多個(gè)類加載器:
CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader是Tomcat自己定義的類加載器,它們分別加載/common/* 、/server/*、/shared/**和/WebApp/WEB-INF/*中的Java類庫。其中WebApp類加載器和JSP類加載器通常會存在多個(gè)實(shí)例,每一個(gè)Web應(yīng)用程序?qū)?yīng)一個(gè)WebApp類加載器,每一個(gè)JSP文件對應(yīng)一個(gè)JSP類加載器。
CommonClassLoader能加載的類都可以被CatalinaClassLoader和SharedClassLoader使用,而CatalinaClassLoader和SharedClassLoader自己能加載的類則與對方相互隔離。WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個(gè)WebAppClassLoader實(shí)例之間相互隔離。而JasperLoader的加載范圍僅是這個(gè)JSP文件編譯出來的那一個(gè)Class,它出現(xiàn)的目的就是被丟棄。當(dāng)服務(wù)器檢測到JSP文件被修改時(shí),會替換掉目前的JasperLoader的實(shí)例,并通過再建立一個(gè)新的JSP類加載器來實(shí)現(xiàn)JSP文件的HotSwap功能。
特殊場景
前文提到過一個(gè)場景,如果有5個(gè)Web應(yīng)用程序都是用Spring來進(jìn)行組織和管理的話,可以把Spring放到Common或Shared目錄下讓這些程序共享。Spring要對用戶程序的類進(jìn)行管理,自然要能訪問到用戶程序的類,而用戶程序放在/WebApp/WEB-INF目錄中,這時(shí)就需要破壞雙親委派模型,使用線程上下文類加載器來完成這一工作了。
OSGi:類加載器的靈活運(yùn)用
OSGi(Open Service Gateway Initiative)是OSGi聯(lián)盟制定的一個(gè)基于Java語言的動態(tài)模塊化規(guī)范,現(xiàn)在成為了Java“事實(shí)上”的模塊化標(biāo)準(zhǔn)。它為開發(fā)人員提供了面向服務(wù)和基于組件的運(yùn)行環(huán)境,并提供標(biāo)準(zhǔn)的方式用來管理軟件的生命周期。OSGi 已經(jīng)被實(shí)現(xiàn)和部署在很多產(chǎn)品上,在開源社區(qū)也得到了廣泛的支持,其中最為著名的應(yīng)用莫過于大家都很熟悉的Eclipse IDE。
OSGi 中的每個(gè)模塊(bundle)都包含?Java Package和Class。模塊可以聲明它所依賴的需要導(dǎo)入(import)的其它模塊的 Java 包和類(通過?Import-Package),也可以聲明導(dǎo)出(export)自己的包和類,供其它模塊使用(通過?Export-Package)。也就是說需要能夠隱藏和共享一個(gè)模塊中的某些 Java 包和類。這是通過 OSGi 特有的類加載器機(jī)制來實(shí)現(xiàn)的。
OSGi 中的每個(gè)模塊都有對應(yīng)的一個(gè)類加載器,它負(fù)責(zé)加載模塊自己包含的 Java 包和類。當(dāng)它需要加載 Java 核心庫的類時(shí)(以 java開頭的包和類),它會代理給父類加載器(通常是啟動類加載器)來完成。當(dāng)它需要加載所導(dǎo)入的 Java 類時(shí),它會代理給導(dǎo)出此 Java 類的模塊來完成加載。模塊也可以顯式的聲明某些 Java 包和類,必須由父類加載器來加載。只需要設(shè)置系統(tǒng)屬性?org.osgi.framework.bootdelegation的值即可。
假設(shè)有兩個(gè)模塊 bundleA 和 bundleB,它們都有自己對應(yīng)的類加載器 ClassLoaderA 和 ClassLoaderB。在 bundleA 中包含類 com.bundleA.Sample,并且該類被聲明為導(dǎo)出的,也就是說可以被其它模塊所使用的。bundleB 聲明了導(dǎo)入 bundleA 提供的類?com.bundleA.Sample,并包含一個(gè)類?com.bundleB.NewSample繼承自?com.bundleA.Sample。在 bundleB 啟動的時(shí)候,其類加載器 classLoaderB 需要加載類?com.bundleB.NewSample,進(jìn)而需要加載類?com.bundleA.Sample。由于 bundleB 聲明了類?com.bundleA.Sample是導(dǎo)入的,classLoaderB 把加載類?com.bundleA.Sample的工作代理給導(dǎo)出該類的 bundleA 的類加載器 ClassLoaderA。ClassLoaderA 在其模塊內(nèi)部查找類?com.bundleA.Sample并定義它,所得到的類?com.bundleA.Sample實(shí)例就可以被所有聲明導(dǎo)入了此類的模塊使用。對于以 java開頭的類,都是由父類加載器來加載的。如果聲明了系統(tǒng)屬性?org.osgi.framework.bootdelegation=com.example.core.*,那么對于包?com.example.core中的類,都是由父類加載器來完成的。 OSGi 模塊的這種類加載器結(jié)構(gòu),使得一個(gè)類的不同版本可以共存在 Java 虛擬機(jī)中,帶來了很大的靈活性。不過它的這種不同,也會給開發(fā)人員帶來一些麻煩,尤其當(dāng)模塊需要使用第三方提供的庫的時(shí)候。下面提供幾條比較好的建議:
- 如果一個(gè)類庫只有一個(gè)模塊使用,把該類庫的 jar 包放在模塊中,在 Bundle-ClassPath中指明即可。
- 如果一個(gè)類庫被多個(gè)模塊共用,可以為這個(gè)類庫單獨(dú)的創(chuàng)建一個(gè)模塊,把其它模塊需要用到的 Java 包聲明為導(dǎo)出的。其它模塊聲明導(dǎo)入這些類。
- 如果類庫提供了 SPI 接口,并且利用線程上下文類加載器來加載 SPI 實(shí)現(xiàn)的 Java 類,有可能會找不到 Java 類。如果出現(xiàn)了 NoClassDefFoundError異常,首先檢查當(dāng)前線程的上下文類加載器是否正確。通過?Thread.currentThread().getContextClassLoader()就可以得到該類加載器。該類加載器應(yīng)該是該模塊對應(yīng)的類加載器。如果不是的話,可以首先通過?class.getClassLoader()來得到模塊對應(yīng)的類加載器,再通過?Thread.currentThread().setContextClassLoader()來設(shè)置當(dāng)前線程的上下文類加載器。
參考資料
- 《深入理解Java虛擬機(jī)——JVM高級特性與最佳實(shí)踐》-周志明
- 深入探討 Java 類加載器-成富
總結(jié)
以上是生活随笔為你收集整理的深入理解JVM(6)——类加载器的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深入理解JVM(5)——虚拟机类加载机制
- 下一篇: JVM垃圾回收算法 总结及汇总