JAVA類(lèi)裝載方式罪既,有兩種:
1.隱式裝載腹暖, 程序在運(yùn)行過(guò)程中當(dāng)碰到通過(guò)new 等方式生成對(duì)象時(shí)汇在,隱式調(diào)用類(lèi)裝載器加載對(duì)應(yīng)的類(lèi)到j(luò)vm中。 2.顯式裝載脏答, 通過(guò)class.forname()等方法糕殉,顯式加載需要的類(lèi)
類(lèi)加載的動(dòng)態(tài)性體現(xiàn):
一個(gè)應(yīng)用程序總是由n多個(gè)類(lèi)組成亩鬼,Java程序啟動(dòng)時(shí),并不是一次把所有的類(lèi)全部加載后再運(yùn)行阿蝶,它總是先把保證程序運(yùn)行的基礎(chǔ)類(lèi)一次性加載到j(luò)vm中雳锋,其它類(lèi)等到j(luò)vm用到的時(shí)候再加載,這樣的好處是節(jié)省了內(nèi)存的開(kāi)銷(xiāo)羡洁,因?yàn)閖ava最早就是為嵌入式系統(tǒng)而設(shè)計(jì)的玷过,內(nèi)存寶貴,這是一種可以理解的機(jī)制筑煮,而用到時(shí)再加載這也是java動(dòng)態(tài)性的一種體現(xiàn)
java類(lèi)裝載器
JDK 默認(rèn)提供了如下幾種ClassLoader
Bootstrp loader
Bootstrp加載器是用C++語(yǔ)言寫(xiě)的辛蚊,它是在Java虛擬機(jī)啟動(dòng)后初始化的,它主要負(fù)責(zé)加載%JAVA_HOME%/jre/lib,-Xbootclasspath參數(shù)指定的路徑以及%JAVA_HOME%/jre/classes中的類(lèi)真仲。
ExtClassLoader
Bootstrp loader加載ExtClassLoader,并且將ExtClassLoader的父加載器設(shè)置為Bootstrp loader.ExtClassLoader是用Java寫(xiě)的袋马,具體來(lái)說(shuō)就是 sun.misc.Launcher$ExtClassLoader,ExtClassLoader主要加載%JAVA_HOME%/jre/lib/ext袒餐,此路徑下的所有classes目錄以及java.ext.dirs系統(tǒng)變量指定的路徑中類(lèi)庫(kù)飞蛹。
AppClassLoader
Bootstrp loader加載完ExtClassLoader后,就會(huì)加載AppClassLoader,并且將AppClassLoader的父加載器指定為 ExtClassLoader灸眼。AppClassLoader也是用Java寫(xiě)成的卧檐,它的實(shí)現(xiàn)類(lèi)是 sun.misc.Launcher$AppClassLoader,另外我們知道ClassLoader中有個(gè)getSystemClassLoader方法,此方法返回的正是AppclassLoader.AppClassLoader主要負(fù)責(zé)加載classpath所指定的位置的類(lèi)或者是jar文檔焰宣,它也是Java程序默認(rèn)的類(lèi)加載器霉囚。
綜上所述,它們之間的關(guān)系可以通過(guò)下圖形象的描述:
為什么要有三個(gè)類(lèi)加載器匕积,一方面是分工盈罐,各自負(fù)責(zé)各自的區(qū)塊,另一方面為了實(shí)現(xiàn)委托模型闪唆。
?類(lèi)加載器之間是如何協(xié)調(diào)工作的
前面說(shuō)了盅粪,java中有三個(gè)類(lèi)加載器,問(wèn)題就來(lái)了悄蕾,碰到一個(gè)類(lèi)需要加載時(shí)票顾,它們之間是如何協(xié)調(diào)工作的,即java是如何區(qū)分一個(gè)類(lèi)該由哪個(gè)類(lèi)加載器來(lái)完成呢帆调。 在這里java采用了委托模型機(jī)制奠骄,這個(gè)機(jī)制簡(jiǎn)單來(lái)講,就是“類(lèi)裝載器有載入類(lèi)的需求時(shí)番刊,會(huì)先請(qǐng)示其Parent使用其搜索路徑幫忙載入含鳞,如果Parent 找不到,那么才由自己依照自己的搜索路徑搜索類(lèi)”
下面舉一個(gè)例子來(lái)說(shuō)明,為了更好的理解芹务,先弄清楚幾行代碼:
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Public class Test{
????Public static void main(String[] arg){
??????ClassLoader c??= Test.class.getClassLoader();??//獲取Test類(lèi)的類(lèi)加載器
????????System.out.println(c);
??????ClassLoader c1 = c.getParent();??//獲取c這個(gè)類(lèi)加載器的父類(lèi)加載器
????????System.out.println(c1);
??????ClassLoader c2 = c1.getParent();//獲取c1這個(gè)類(lèi)加載器的父類(lèi)加載器
????????System.out.println(c2);
??}
}
運(yùn)行結(jié)果:
Java
1
2
3
4
5
……AppClassLoader……
……ExtClassLoader……
Null
可以看出Test是由AppClassLoader加載器加載的蝉绷,AppClassLoader的Parent加載器是ExtClassLoader,但是ExtClassLoader的Parent為null是怎么回事呵鸭廷,朋友們留意的話,前面有提到Bootstrap Loader是用C++語(yǔ)言寫(xiě)的潜必,依java的觀點(diǎn)來(lái)看靴姿,邏輯上并不存在Bootstrap Loader的類(lèi)實(shí)體,所以在java程序代碼里試圖打印出其內(nèi)容時(shí)磁滚,我們就會(huì)看到輸出為null佛吓。
類(lèi)裝載器ClassLoader(一個(gè)抽象類(lèi))描述一下JVM加載class文件的原理機(jī)制
類(lèi)裝載器就是尋找類(lèi)或接口字節(jié)碼文件進(jìn)行解析并構(gòu)造JVM內(nèi)部對(duì)象表示的組件,在java中類(lèi)裝載器把一個(gè)類(lèi)裝入JVM垂攘,經(jīng)過(guò)以下步驟:
1维雇、裝載:查找和導(dǎo)入Class文件 2、鏈接:其中解析步驟是可以選擇的 (a)檢查:檢查載入的class文件數(shù)據(jù)的正確性 (b)準(zhǔn)備:給類(lèi)的靜態(tài)變量分配存儲(chǔ)空間 (c)解析:將符號(hào)引用轉(zhuǎn)成直接引用 3晒他、初始化:對(duì)靜態(tài)變量吱型,靜態(tài)代碼塊執(zhí)行初始化工作
類(lèi)裝載工作由ClassLoder和其子類(lèi)負(fù)責(zé)。JVM在運(yùn)行時(shí)會(huì)產(chǎn)生三個(gè)ClassLoader:根裝載器陨仅,ExtClassLoader(擴(kuò)展類(lèi)裝載器)和AppClassLoader津滞,其中根裝載器不是ClassLoader的子類(lèi),由C++編寫(xiě)灼伤,因此在java中看不到他触徐,負(fù)責(zé)裝載JRE的核心類(lèi)庫(kù),如JRE目錄下的rt.jar,charsets.jar等狐赡。ExtClassLoader是ClassLoder的子類(lèi)撞鹉,負(fù)責(zé)裝載JRE擴(kuò)展目錄ext下的jar類(lèi)包;AppClassLoader負(fù)責(zé)裝載classpath路徑下的類(lèi)包颖侄,這三個(gè)類(lèi)裝載器存在父子層級(jí)關(guān)系****鸟雏,即根裝載器是ExtClassLoader的父裝載器,ExtClassLoader是AppClassLoader的父裝載器览祖。默認(rèn)情況下使用AppClassLoader裝載應(yīng)用程序的類(lèi)
Java裝載類(lèi)使用“全盤(pán)負(fù)責(zé)委托機(jī)制”孝鹊。“全盤(pán)負(fù)責(zé)”是指當(dāng)一個(gè)ClassLoder裝載一個(gè)類(lèi)時(shí)展蒂,除非顯示的使用另外一個(gè)ClassLoder又活,該類(lèi)所依賴及引用的類(lèi)也由這個(gè)ClassLoder載入;“委托機(jī)制”是指先委托父類(lèi)裝載器尋找目標(biāo)類(lèi)玄货,只有在找不到的情況下才從自己的類(lèi)路徑中查找并裝載目標(biāo)類(lèi)。這一點(diǎn)是從安全方面考慮的悼泌,試想如果一個(gè)人寫(xiě)了一個(gè)惡意的基礎(chǔ)類(lèi)(如java.lang.String)并加載到JVM將會(huì)引起嚴(yán)重的后果松捉,但有了全盤(pán)負(fù)責(zé)制,java.lang.String永遠(yuǎn)是由根裝載器來(lái)裝載馆里,避免以上情況發(fā)生 除了JVM默認(rèn)的三個(gè)ClassLoder以外隘世,第三方可以編寫(xiě)自己的類(lèi)裝載器可柿,以實(shí)現(xiàn)一些特殊的需求。類(lèi)文件被裝載解析后丙者,在JVM中都有一個(gè)對(duì)應(yīng)的java.lang.Class對(duì)象复斥,提供了類(lèi)結(jié)構(gòu)信息的描述。數(shù)組械媒,枚舉及基本數(shù)據(jù)類(lèi)型目锭,甚至void都擁有對(duì)應(yīng)的Class對(duì)象。Class類(lèi)沒(méi)有public的構(gòu)造方法纷捞,Class對(duì)象是在裝載類(lèi)時(shí)由JVM通過(guò)調(diào)用類(lèi)裝載器中的defineClass()方法自動(dòng)構(gòu)造的痢虹。
為什么要使用這種雙親委托模式呢?
因?yàn)檫@樣可以避免重復(fù)加載主儡,當(dāng)父親已經(jīng)加載了該類(lèi)的時(shí)候奖唯,就沒(méi)有必要子ClassLoader再加載一次。
考慮到安全因素糜值,我們?cè)囅胍幌路峤荩绻皇褂眠@種委托模式,那我們就可以隨時(shí)使用自定義的String來(lái)動(dòng)態(tài)替代java核心api中定義類(lèi)型寂汇,這樣會(huì)存在非常大的安全隱患病往,而雙親委托的方式,就可以避免這種情況健无,因?yàn)镾tring已經(jīng)在啟動(dòng)時(shí)被加載荣恐,所以用戶自定義類(lèi)是無(wú)法加載一個(gè)自定義的ClassLoader。
思考:假如我們自己寫(xiě)了一個(gè)java.lang.String的類(lèi)累贤,我們是否可以替換調(diào)JDK本身的類(lèi)叠穆?
答案是否定的。我們不能實(shí)現(xiàn)臼膏。為什么呢硼被?我看很多網(wǎng)上解釋是說(shuō)雙親委托機(jī)制解決這個(gè)問(wèn)題,其實(shí)不是非常的準(zhǔn)確渗磅。因?yàn)殡p親委托機(jī)制是可以打破的嚷硫,你完全可以自己寫(xiě)一個(gè)classLoader來(lái)加載自己寫(xiě)的java.lang.String類(lèi),但是你會(huì)發(fā)現(xiàn)也不會(huì)加載成功始鱼,具體就是因?yàn)獒槍?duì)java.*開(kāi)頭的類(lèi)仔掸,jvm的實(shí)現(xiàn)中已經(jīng)保證了必須由bootstrp來(lái)加載。
?定義自已的ClassLoader
既然JVM已經(jīng)提供了默認(rèn)的類(lèi)加載器医清,為什么還要定義自已的類(lèi)加載器呢起暮?
因?yàn)镴ava中提供的默認(rèn)ClassLoader,只加載指定目錄下的jar和class会烙,如果我們想加載其它位置的類(lèi)或jar時(shí)负懦,比如:我要加載網(wǎng)絡(luò)上的一個(gè)class文件筒捺,通過(guò)動(dòng)態(tài)加載到內(nèi)存之后,要調(diào)用這個(gè)類(lèi)中的方法實(shí)現(xiàn)我的業(yè)務(wù)邏輯纸厉。在這樣的情況下系吭,默認(rèn)的ClassLoader就不能滿足我們的需求了,所以需要定義自己的ClassLoader颗品。
定義自已的類(lèi)加載器分為兩步:
1肯尺、繼承java.lang.ClassLoader
2、重寫(xiě)父類(lèi)的findClass方法
讀者可能在這里有疑問(wèn)抛猫,父類(lèi)有那么多方法蟆盹,為什么偏偏只重寫(xiě)findClass方法?
因?yàn)镴DK已經(jīng)在loadClass方法中幫我們實(shí)現(xiàn)了ClassLoader搜索類(lèi)的算法闺金,當(dāng)在loadClass方法中搜索不到類(lèi)時(shí)逾滥,loadClass方法就會(huì)調(diào)用findClass方法來(lái)搜索類(lèi),所以我們只需重寫(xiě)該方法即可败匹。如沒(méi)有特殊的要求寨昙,一般不建議重寫(xiě)loadClass搜索類(lèi)的算法。
線程上下文類(lèi)加載器
線程上下文類(lèi)加載器(context class loader)是從 JDK 1.2 開(kāi)始引入的掀亩。類(lèi) java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用來(lái)獲取和設(shè)置線程的上下文類(lèi)加載器舔哪。如果沒(méi)有通過(guò) setContextClassLoader(ClassLoader cl)方法進(jìn)行設(shè)置的話,線程將繼承其父線程的上下文類(lèi)加載器槽棍。Java 應(yīng)用運(yùn)行的初始線程的上下文類(lèi)加載器是系統(tǒng)類(lèi)加載器捉蚤。在線程中運(yùn)行的代碼可以通過(guò)此類(lèi)加載器來(lái)加載類(lèi)和資源。
前面提到的類(lèi)加載器的代理模式并不能解決 Java 應(yīng)用開(kāi)發(fā)中會(huì)遇到的類(lèi)加載器的全部問(wèn)題炼七。Java 提供了很多服務(wù)提供者接口(Service Provider Interface缆巧,SPI),允許第三方為這些接口提供實(shí)現(xiàn)豌拙。常見(jiàn)的 SPI 有 JDBC陕悬、JCE、JNDI按傅、JAXP 和 JBI 等捉超。這些 SPI 的接口由 Java 核心庫(kù)來(lái)提供,如 JAXP 的 SPI 接口定義包含在 javax.xml.parsers包中唯绍。這些 SPI 的實(shí)現(xiàn)代碼很可能是作為 Java 應(yīng)用所依賴的 jar 包被包含進(jìn)來(lái)拼岳,可以通過(guò)類(lèi)路徑(CLASSPATH)來(lái)找到,如實(shí)現(xiàn)了 JAXP SPI 的 Apache Xerces所包含的 jar 包况芒。SPI 接口中的代碼經(jīng)常需要加載具體的實(shí)現(xiàn)類(lèi)惜纸。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory類(lèi)中的 newInstance()方法用來(lái)生成一個(gè)新的 DocumentBuilderFactory的實(shí)例。這里的實(shí)例的真正的類(lèi)是繼承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的實(shí)現(xiàn)所提供的堪簿。如在 Apache Xerces 中,實(shí)現(xiàn)的類(lèi)是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl皮壁。而問(wèn)題在于椭更,SPI 的接口是 Java 核心庫(kù)的一部分,是由引導(dǎo)類(lèi)加載器來(lái)加載的蛾魄;SPI 實(shí)現(xiàn)的 Java 類(lèi)一般是由系統(tǒng)類(lèi)加載器來(lái)加載的虑瀑。引導(dǎo)類(lèi)加載器是無(wú)法找到 SPI 的實(shí)現(xiàn)類(lèi)的,因?yàn)樗患虞d Java 的核心庫(kù)滴须。它也不能代理給系統(tǒng)類(lèi)加載器舌狗,因?yàn)樗窍到y(tǒng)類(lèi)加載器的祖先類(lèi)加載器。也就是說(shuō)扔水,類(lèi)加載器的代理模式無(wú)法解決這個(gè)問(wèn)題痛侍。
線程上下文類(lèi)加載器正好解決了這個(gè)問(wèn)題。如果不做任何的設(shè)置魔市,Java 應(yīng)用的線程的上下文類(lèi)加載器默認(rèn)就是系統(tǒng)上下文類(lèi)加載器主届。在 SPI 接口的代碼中使用線程上下文類(lèi)加載器,就可以成功的加載到 SPI 實(shí)現(xiàn)的類(lèi)待德。線程上下文類(lèi)加載器在很多 SPI 的實(shí)現(xiàn)中都會(huì)用到君丁。
類(lèi)加載器與Web容器
對(duì)于運(yùn)行在 Java EE容器中的 Web 應(yīng)用來(lái)說(shuō),類(lèi)加載器的實(shí)現(xiàn)方式與一般的 Java 應(yīng)用有所不同将宪。不同的 Web 容器的實(shí)現(xiàn)方式也會(huì)有所不同绘闷。以 Apache Tomcat 來(lái)說(shuō),每個(gè) Web 應(yīng)用都有一個(gè)對(duì)應(yīng)的類(lèi)加載器實(shí)例较坛。該類(lèi)加載器也使用代理模式印蔗,所不同的是它是首先嘗試去加載某個(gè)類(lèi),如果找不到再代理給父類(lèi)加載器燎潮。這與一般類(lèi)加載器的順序是相反的喻鳄。這是 Java Servlet 規(guī)范中的推薦做法,其目的是使得 Web 應(yīng)用自己的類(lèi)的優(yōu)先級(jí)高于 Web 容器提供的類(lèi)确封。這種代理模式的一個(gè)例外是:Java 核心庫(kù)的類(lèi)是不在查找范圍之內(nèi)的除呵。這也是為了保證 Java 核心庫(kù)的類(lèi)型安全。
絕大多數(shù)情況下爪喘,Web 應(yīng)用的開(kāi)發(fā)人員不需要考慮與類(lèi)加載器相關(guān)的細(xì)節(jié)颜曾。下面給出幾條簡(jiǎn)單的原則:
(1)每個(gè) Web 應(yīng)用自己的 Java 類(lèi)文件和使用的庫(kù)的 jar 包,分別放在 WEB-INF/classes和 WEB-INF/lib目錄下面秉剑。
(2)多個(gè)應(yīng)用共享的 Java 類(lèi)文件和 jar 包泛豪,分別放在 Web 容器指定的由所有 Web 應(yīng)用共享的目錄下面。
(3)當(dāng)出現(xiàn)找不到類(lèi)的錯(cuò)誤時(shí),檢查當(dāng)前類(lèi)的類(lèi)加載器和當(dāng)前線程的上下文類(lèi)加載器是否正確诡曙。
類(lèi)加載器與OSGi
OSGi是 Java 上的動(dòng)態(tài)模塊系統(tǒng)臀叙。它為開(kāi)發(fā)人員提供了面向服務(wù)和基于組件的運(yùn)行環(huán)境,并提供標(biāo)準(zhǔn)的方式用來(lái)管理軟件的生命周期价卤。OSGi 已經(jīng)被實(shí)現(xiàn)和部署在很多產(chǎn)品上劝萤,在開(kāi)源社區(qū)也得到了廣泛的支持。Eclipse就是基于OSGi 技術(shù)來(lái)構(gòu)建的慎璧。
OSGi 中的每個(gè)模塊(bundle)都包含 Java 包和類(lèi)床嫌。模塊可以聲明它所依賴的需要導(dǎo)入(import)的其它模塊的 Java 包和類(lèi)(通過(guò) Import-Package),也可以聲明導(dǎo)出(export)自己的包和類(lèi)胸私,供其它模塊使用(通過(guò) Export-Package)厌处。也就是說(shuō)需要能夠隱藏和共享一個(gè)模塊中的某些 Java 包和類(lèi)。這是通過(guò) OSGi 特有的類(lèi)加載器機(jī)制來(lái)實(shí)現(xiàn)的岁疼。OSGi 中的每個(gè)模塊都有對(duì)應(yīng)的一個(gè)類(lèi)加載器阔涉。它負(fù)責(zé)加載模塊自己包含的 Java 包和類(lèi)。當(dāng)它需要加載 Java 核心庫(kù)的類(lèi)時(shí)(以 java開(kāi)頭的包和類(lèi))捷绒,它會(huì)代理給父類(lèi)加載器(通常是啟動(dòng)類(lèi)加載器)來(lái)完成洒敏。當(dāng)它需要加載所導(dǎo)入的 Java 類(lèi)時(shí),它會(huì)代理給導(dǎo)出此 Java 類(lèi)的模塊來(lái)完成加載疙驾。模塊也可以顯式的聲明某些 Java 包和類(lèi)凶伙,必須由父類(lèi)加載器來(lái)加載。只需要設(shè)置系統(tǒng)屬性 org.osgi.framework.bootdelegation的值即可它碎。
假設(shè)有兩個(gè)模塊 bundleA 和 bundleB函荣,它們都有自己對(duì)應(yīng)的類(lèi)加載器 classLoaderA 和 classLoaderB。在 bundleA 中包含類(lèi) com.bundleA.Sample扳肛,并且該類(lèi)被聲明為導(dǎo)出的傻挂,也就是說(shuō)可以被其它模塊所使用的。bundleB 聲明了導(dǎo)入 bundleA 提供的類(lèi) com.bundleA.Sample挖息,并包含一個(gè)類(lèi) com.bundleB.NewSample繼承自 com.bundleA.Sample金拒。在 bundleB 啟動(dòng)的時(shí)候,其類(lèi)加載器 classLoaderB 需要加載類(lèi) com.bundleB.NewSample套腹,進(jìn)而需要加載類(lèi) com.bundleA.Sample绪抛。由于 bundleB 聲明了類(lèi) com.bundleA.Sample是導(dǎo)入的,classLoaderB 把加載類(lèi) com.bundleA.Sample的工作代理給導(dǎo)出該類(lèi)的 bundleA 的類(lèi)加載器 classLoaderA电禀。classLoaderA 在其模塊內(nèi)部查找類(lèi) com.bundleA.Sample并定義它幢码,所得到的類(lèi) com.bundleA.Sample實(shí)例就可以被所有聲明導(dǎo)入了此類(lèi)的模塊使用。對(duì)于以 java開(kāi)頭的類(lèi)尖飞,都是由父類(lèi)加載器來(lái)加載的症副。如果聲明了系統(tǒng)屬性 org.osgi.framework.bootdelegation=com.example.core.*店雅,那么對(duì)于包 com.example.core中的類(lèi),都是由父類(lèi)加載器來(lái)完成的贞铣。
OSGi 模塊的這種類(lèi)加載器結(jié)構(gòu)闹啦,使得一個(gè)類(lèi)的不同版本可以共存在 Java 虛擬機(jī)中,帶來(lái)了很大的靈活性辕坝。不過(guò)它的這種不同亥揖,也會(huì)給開(kāi)發(fā)人員帶來(lái)一些麻煩,尤其當(dāng)模塊需要使用第三方提供的庫(kù)的時(shí)候圣勒。下面提供幾條比較好的建議:
(1)如果一個(gè)類(lèi)庫(kù)只有一個(gè)模塊使用,把該類(lèi)庫(kù)的 jar 包放在模塊中摧扇,在 Bundle-ClassPath中指明即可圣贸。
(2)如果一個(gè)類(lèi)庫(kù)被多個(gè)模塊共用,可以為這個(gè)類(lèi)庫(kù)單獨(dú)的創(chuàng)建一個(gè)模塊扛稽,把其它模塊需要用到的 Java 包聲明為導(dǎo)出的吁峻。其它模塊聲明導(dǎo)入這些類(lèi)。
(3)如果類(lèi)庫(kù)提供了 SPI 接口在张,并且利用線程上下文類(lèi)加載器來(lái)加載 SPI 實(shí)現(xiàn)的 Java 類(lèi)用含,有可能會(huì)找不到 Java 類(lèi)。如果出現(xiàn)了 NoClassDefFoundError異常帮匾,首先檢查當(dāng)前線程的上下文類(lèi)加載器是否正確啄骇。通過(guò) Thread.currentThread().getContextClassLoader()就可以得到該類(lèi)加載器。該類(lèi)加載器應(yīng)該是該模塊對(duì)應(yīng)的類(lèi)加載器瘟斜。如果不是的話缸夹,可以首先通過(guò) class.getClassLoader()來(lái)得到模塊對(duì)應(yīng)的類(lèi)加載器,再通過(guò) Thread.currentThread().setContextClassLoader()來(lái)設(shè)置當(dāng)前線程的上下文類(lèi)加載器螺句。