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