JVM類加載器ClassLoader
JAVA類裝載方式
1.隱式裝載, 程序在運行過程中當(dāng)碰到通過new 等方式生成對象時,隱式調(diào)用類裝載器加載對應(yīng)的類到j(luò)vm中。
2.顯式裝載, 通過class.forname()等方法,顯式加載需要的類
一個應(yīng)用程序總是由n多個類組成威创,Java程序啟動時,并不是一次把所有的類全部加載后再運行谎懦,它總是先把保證程序運行的基礎(chǔ)類一次性加載到j(luò)vm中肚豺,其它類等到j(luò)vm用到的時候再加載,這樣的好處是節(jié)省了內(nèi)存的開銷界拦,因為java最早就是為嵌入式系統(tǒng)而設(shè)計的吸申,內(nèi)存寶貴,這是一種可以理解的機制,而用到時再加載這也是java動態(tài)性的一種體現(xiàn)
java類裝載器
1截碴、Bootstrp loader
Bootstrp加載器是用C++語言寫的梳侨,它是在Java虛擬機啟動后初始化的,它主要負責(zé)加載%JAVA_HOME%/jre/lib,-Xbootclasspath參數(shù)指定的路徑以及%JAVA_HOME%/jre/classes中的類日丹。
2走哺、ExtClassLoader
Bootstrp loader加載ExtClassLoader,并且將ExtClassLoader的父加載器設(shè)置為Bootstrp loader.ExtClassLoader是用Java寫的,具體來說就是 sun.misc.Launcher$ExtClassLoader哲虾,ExtClassLoader主要加載%JAVA_HOME%/jre/lib/ext丙躏,此路徑下的所有classes目錄以及java.ext.dirs系統(tǒng)變量指定的路徑中類庫。
3束凑、AppClassLoader
Bootstrp loader加載完ExtClassLoader后晒旅,就會加載AppClassLoader,并且將AppClassLoader的父加載器指定為 ExtClassLoader。AppClassLoader也是用Java寫成的湘今,它的實現(xiàn)類是 sun.misc.Launcher$AppClassLoader敢朱,另外我們知道ClassLoader中有個getSystemClassLoader方法,此方法返回的正是AppclassLoader.AppClassLoader主要負責(zé)加載classpath所指定的位置的類或者是jar文檔,它也是Java程序默認的類加載器摩瞎。
類加載器之間是如何協(xié)調(diào)工作的
前面說了,java中有三個類加載器孝常,問題就來了旗们,碰到一個類需要加載時,它們之間是如何協(xié)調(diào)工作的构灸,即java是如何區(qū)分一個類該由哪個類加載器來完成呢上渴。 在這里java采用了委托模型機制,這個機制簡單來講喜颁,就是“類裝載器有載入類的需求時稠氮,會先請示其Parent使用其搜索路徑幫忙載入,如果Parent 找不到,那么才由自己依照自己的搜索路徑搜索類”
下面舉一個例子來說明半开,為了更好的理解隔披,先弄清楚幾行代碼:
Java
Public class Test{
? ? Public static void main(String[] arg){
? ? ? ClassLoader c? = Test.class.getClassLoader();? //獲取Test類的類加載器
? ? ? ? System.out.println(c);
? ? ? ClassLoader c1 = c.getParent();? //獲取c這個類加載器的父類加載器
? ? ? ? System.out.println(c1);
? ? ? ClassLoader c2 = c1.getParent();//獲取c1這個類加載器的父類加載器
? ? ? ? System.out.println(c2);
? }
}
運行結(jié)果:
……AppClassLoader……
……ExtClassLoader……
Null
可以看出Test是由AppClassLoader加載器加載的,AppClassLoader的Parent加載器是ExtClassLoader,但是ExtClassLoader的Parent為null是怎么回事呵寂拆,朋友們留意的話奢米,前面有提到Bootstrap Loader是用C++語言寫的,依java的觀點來看纠永,邏輯上并不存在Bootstrap Loader的類實體鬓长,所以在java程序代碼里試圖打印出其內(nèi)容時,我們就會看到輸出為null尝江。
JVM加載class文件的原理機制
類裝載器就是尋找類或接口字節(jié)碼文件進行解析并構(gòu)造JVM內(nèi)部對象表示的組件涉波,在java中類裝載器把一個類裝入JVM,經(jīng)過以下步驟:
1、裝載:查找和導(dǎo)入Class文件
2啤覆、鏈接:其中解析步驟是可以選擇的
(a)檢查:檢查載入的class文件數(shù)據(jù)的正確性
(b)準(zhǔn)備:給類的靜態(tài)變量分配存儲空間
(c)解析:將符號引用轉(zhuǎn)成直接引用
3苍日、初始化:對靜態(tài)變量,靜態(tài)代碼塊執(zhí)行初始化工作
類裝載工作由ClassLoder和其子類負責(zé)城侧。JVM在運行時會產(chǎn)生三個ClassLoader:根裝載器易遣,ExtClassLoader(擴展類裝載器)和AppClassLoader,其中根裝載器不是ClassLoader的子類嫌佑,由C++編寫豆茫,因此在java中看不到他,負責(zé)裝載JRE的核心類庫屋摇,如JRE目錄下的rt.jar,charsets.jar等揩魂。ExtClassLoader是ClassLoder的子類,負責(zé)裝載JRE擴展目錄ext下的jar類包炮温;AppClassLoader負責(zé)裝載classpath路徑下的類包火脉,這三個類裝載器存在父子層級關(guān)系****,即根裝載器是ExtClassLoader的父裝載器柒啤,ExtClassLoader是AppClassLoader的父裝載器倦挂。默認情況下使用AppClassLoader裝載應(yīng)用程序的類
Java裝載類使用“全盤負責(zé)委托機制”〉9“全盤負責(zé)”是指當(dāng)一個ClassLoder裝載一個類時方援,除非顯示的使用另外一個ClassLoder,該類所依賴及引用的類也由這個ClassLoder載入涛癌;“委托機制”是指先委托父類裝載器尋找目標(biāo)類犯戏,只有在找不到的情況下才從自己的類路徑中查找并裝載目標(biāo)類。這一點是從安全方面考慮的拳话,試想如果一個人寫了一個惡意的基礎(chǔ)類(如java.lang.String)并加載到JVM將會引起嚴(yán)重的后果先匪,但有了全盤負責(zé)制,java.lang.String永遠是由根裝載器來裝載弃衍,避免以上情況發(fā)生 除了JVM默認的三個ClassLoder以外呀非,第三方可以編寫自己的類裝載器,以實現(xiàn)一些特殊的需求笨鸡。類文件被裝載解析后姜钳,在JVM中都有一個對應(yīng)的java.lang.Class對象,提供了類結(jié)構(gòu)信息的描述形耗。數(shù)組哥桥,枚舉及基本數(shù)據(jù)類型,甚至void都擁有對應(yīng)的Class對象激涤。Class類沒有public的構(gòu)造方法拟糕,Class對象是在裝載類時由JVM通過調(diào)用類裝載器中的defineClass()方法自動構(gòu)造的判呕。
雙親委托
一個類加載器查找class和resource時,是通過“委托模式”進行的送滞,它首先判斷這個class是不是已經(jīng)加載成功侠草,如果沒有的話它并不是自己進行查找,而是先通過父加載器犁嗅,然后遞歸下去边涕,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了褂微,直接返回功蜓,如果沒有找到,則一級一級返回宠蚂,最后到達自身去查找這些對象式撼。這種機制就叫做雙親委托。
Java中ClassLoader的加載采用了雙親委托機制求厕,采用雙親委托機制加載類的時候采用如下的幾個步驟:
當(dāng)前ClassLoader首先從自己已經(jīng)加載的類中查詢是否此類已經(jīng)加載著隆,如果已經(jīng)加載則直接返回原來已經(jīng)加載的類。
每個類加載器都有自己的加載緩存呀癣,當(dāng)一個類被加載了以后就會放入緩存美浦,等下次加載的時候就可以直接返回了。
當(dāng)前classLoader的緩存中沒有找到被加載的類的時候项栏,委托父類加載器去加載抵代,父類加載器采用同樣的策略,首先查看自己的緩存忘嫉,然后委托父類的父類去加載,一直到bootstrp ClassLoader.
當(dāng)所有的父類加載器都沒有加載的時候案腺,再由當(dāng)前的類加載器加載庆冕,并將其放入它自己的緩存中,以便下次有加載請求的時候直接返回劈榨。
說到這里大家可能會想访递,Java為什么要采用這樣的委托機制?理解這個問題同辣,我們引入另外一個關(guān)于Classloader的概念“命名空間”拷姿, 它是指要確定某一個類,需要類的全限定名以及加載此類的ClassLoader來共同確定旱函。也就是說即使兩個類的全限定名是相同的响巢,但是因為不同的 ClassLoader加載了此類,那么在JVM中它是不同的類棒妨。明白了命名空間以后踪古,我們再來看看委托模型。采用了委托模型以后加大了不同的 ClassLoader的交互能力,比如上面說的伏穆,我們JDK本生提供的類庫拘泞,比如hashmap,linkedlist等等,這些類由bootstrp 類加載器加載了以后枕扫,無論你程序中有多少個類加載器陪腌,那么這些類其實都是可以共享的,這樣就避免了不同的類加載器加載了同樣名字的不同類以后造成混亂烟瞧。
整個流程可以如下圖所示:
定義自已的ClassLoader
既然JVM已經(jīng)提供了默認的類加載器诗鸭,為什么還要定義自已的類加載器呢?
因為Java中提供的默認ClassLoader燕刻,只加載指定目錄下的jar和class只泼,如果我們想加載其它位置的類或jar時,比如:我要加載網(wǎng)絡(luò)上的一個class文件卵洗,通過動態(tài)加載到內(nèi)存之后请唱,要調(diào)用這個類中的方法實現(xiàn)我的業(yè)務(wù)邏輯。在這樣的情況下过蹂,默認的ClassLoader就不能滿足我們的需求了十绑,所以需要定義自己的ClassLoader。
定義自已的類加載器分為兩步:
1酷勺、繼承java.lang.ClassLoader
2本橙、重寫父類的findClass方法
讀者可能在這里有疑問,父類有那么多方法脆诉,為什么偏偏只重寫findClass方法甚亭?
因為JDK已經(jīng)在loadClass方法中幫我們實現(xiàn)了ClassLoader搜索類的算法,當(dāng)在loadClass方法中搜索不到類時击胜,loadClass方法就會調(diào)用findClass方法來搜索類亏狰,所以我們只需重寫該方法即可。如沒有特殊的要求暇唾,一般不建議重寫loadClass搜索類的算法。
線程上下文類加載器
線程上下文類加載器(context class loader)是從 JDK 1.2 開始引入的辰斋。類 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用來獲取和設(shè)置線程的上下文類加載器策州。如果沒有通過 setContextClassLoader(ClassLoader cl)方法進行設(shè)置的話,線程將繼承其父線程的上下文類加載器宫仗。Java 應(yīng)用運行的初始線程的上下文類加載器是系統(tǒng)類加載器够挂。在線程中運行的代碼可以通過此類加載器來加載類和資源。
前面提到的類加載器的代理模式并不能解決 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)類加載器的祖先類加載器漏健。也就是說,類加載器的代理模式無法解決這個問題橘霎。
線程上下文類加載器正好解決了這個問題漾肮。如果不做任何的設(shè)置,Java 應(yīng)用的線程的上下文類加載器默認就是系統(tǒng)上下文類加載器茎毁。在 SPI 接口的代碼中使用線程上下文類加載器,就可以成功的加載到 SPI 實現(xiàn)的類忱辅。線程上下文類加載器在很多 SPI 的實現(xiàn)中都會用到七蜘。
類加載器與Web容器
對于運行在 Java EE容器中的 Web 應(yīng)用來說,類加載器的實現(xiàn)方式與一般的 Java 應(yīng)用有所不同墙懂。不同的 Web 容器的實現(xiàn)方式也會有所不同橡卤。以 Apache Tomcat 來說,每個 Web 應(yīng)用都有一個對應(yīng)的類加載器實例损搬。該類加載器也使用代理模式碧库,所不同的是它是首先嘗試去加載某個類柜与,如果找不到再代理給父類加載器。這與一般類加載器的順序是相反的嵌灰。這是 Java Servlet 規(guī)范中的推薦做法弄匕,其目的是使得 Web 應(yīng)用自己的類的優(yōu)先級高于 Web 容器提供的類。這種代理模式的一個例外是:Java 核心庫的類是不在查找范圍之內(nèi)的沽瞭。這也是為了保證 Java 核心庫的類型安全迁匠。
絕大多數(shù)情況下,Web 應(yīng)用的開發(fā)人員不需要考慮與類加載器相關(guān)的細節(jié)驹溃。下面給出幾條簡單的原則:
〕巧ァ(1)每個 Web 應(yīng)用自己的 Java 類文件和使用的庫的 jar 包,分別放在 WEB-INF/classes和 WEB-INF/lib目錄下面豌鹤。
⊥龊濉(2)多個應(yīng)用共享的 Java 類文件和 jar 包,分別放在 Web 容器指定的由所有 Web 應(yīng)用共享的目錄下面布疙。
∥霉摺(3)當(dāng)出現(xiàn)找不到類的錯誤時,檢查當(dāng)前類的類加載器和當(dāng)前線程的上下文類加載器是否正確拐辽。
類加載器與OSGi
OSGi是 Java 上的動態(tài)模塊系統(tǒng)拣挪。它為開發(fā)人員提供了面向服務(wù)和基于組件的運行環(huán)境,并提供標(biāo)準(zhǔn)的方式用來管理軟件的生命周期俱诸。OSGi 已經(jīng)被實現(xiàn)和部署在很多產(chǎn)品上菠劝,在開源社區(qū)也得到了廣泛的支持。Eclipse就是基于OSGi 技術(shù)來構(gòu)建的睁搭。
OSGi 中的每個模塊(bundle)都包含 Java 包和類赶诊。模塊可以聲明它所依賴的需要導(dǎo)入(import)的其它模塊的 Java 包和類(通過 Import-Package),也可以聲明導(dǎo)出(export)自己的包和類园骆,供其它模塊使用(通過 Export-Package)舔痪。也就是說需要能夠隱藏和共享一個模塊中的某些 Java 包和類。這是通過 OSGi 特有的類加載器機制來實現(xiàn)的锌唾。OSGi 中的每個模塊都有對應(yīng)的一個類加載器锄码。它負責(zé)加載模塊自己包含的 Java 包和類。當(dāng)它需要加載 Java 核心庫的類時(以 java開頭的包和類)晌涕,它會代理給父類加載器(通常是啟動類加載器)來完成滋捶。當(dāng)它需要加載所導(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ā)人員帶來一些麻煩,尤其當(dāng)模塊需要使用第三方提供的庫的時候仇箱。下面提供幾條比較好的建議:
∠厮 (1)如果一個類庫只有一個模塊使用,把該類庫的 jar 包放在模塊中剂桥,在 Bundle-ClassPath中指明即可忠烛。
(2)如果一個類庫被多個模塊共用权逗,可以為這個類庫單獨的創(chuàng)建一個模塊美尸,把其它模塊需要用到的 Java 包聲明為導(dǎo)出的。其它模塊聲明導(dǎo)入這些類斟薇。
』鹁(3)如果類庫提供了 SPI 接口,并且利用線程上下文類加載器來加載 SPI 實現(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)前線程的上下文類加載器。