JVM類加載器ClassLoader
JAVA類裝載方式
1.隱式裝載, 程序在運行過程中當碰到通過new 等方式生成對象時,隱式調(diào)用類裝載器加載對應的類到jvm中。
2.顯式裝載, 通過class.forname()等方法,顯式加載需要的類
一個應用程序總是由n多個類組成,Java程序啟動時,并不是一次把所有的類全部加載后再運行,它總是先把保證程序運行的基礎類一次性加載到jvm中,其它類等到jvm用到的時候再加載,這樣的好處是節(jié)省了內(nèi)存的開銷,因為java最早就是為嵌入式系統(tǒng)而設計的,內(nèi)存寶貴,這是一種可以理解的機制,而用到時再加載這也是java動態(tài)性的一種體現(xiàn)
java類裝載器

1、Bootstrp loader
Bootstrp加載器是用C++語言寫的,它是在Java虛擬機啟動后初始化的,它主要負責加載%JAVA_HOME%/jre/lib,-Xbootclasspath參數(shù)指定的路徑以及%JAVA_HOME%/jre/classes中的類。
2、ExtClassLoader
Bootstrp loader加載ExtClassLoader,并且將ExtClassLoader的父加載器設置為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主要負責加載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、裝載:查找和導入Class文件
2、鏈接:其中解析步驟是可以選擇的
(a)檢查:檢查載入的class文件數(shù)據(jù)的正確性
(b)準備:給類的靜態(tài)變量分配存儲空間
(c)解析:將符號引用轉(zhuǎn)成直接引用
3、初始化:對靜態(tài)變量,靜態(tài)代碼塊執(zhí)行初始化工作
類裝載工作由ClassLoder和其子類負責。JVM在運行時會產(chǎn)生三個ClassLoader:根裝載器,ExtClassLoader(擴展類裝載器)和AppClassLoader,其中根裝載器不是ClassLoader的子類,由C++編寫,因此在java中看不到他,負責裝載JRE的核心類庫,如JRE目錄下的rt.jar,charsets.jar等。ExtClassLoader是ClassLoder的子類,負責裝載JRE擴展目錄ext下的jar類包;AppClassLoader負責裝載classpath路徑下的類包,這三個類裝載器存在父子層級關系****,即根裝載器是ExtClassLoader的父裝載器,ExtClassLoader是AppClassLoader的父裝載器。默認情況下使用AppClassLoader裝載應用程序的類
Java裝載類使用“全盤負責委托機制”。“全盤負責”是指當一個ClassLoder裝載一個類時,除非顯示的使用另外一個ClassLoder,該類所依賴及引用的類也由這個ClassLoder載入;“委托機制”是指先委托父類裝載器尋找目標類,只有在找不到的情況下才從自己的類路徑中查找并裝載目標類。這一點是從安全方面考慮的,試想如果一個人寫了一個惡意的基礎類(如java.lang.String)并加載到JVM將會引起嚴重的后果,但有了全盤負責制,java.lang.String永遠是由根裝載器來裝載,避免以上情況發(fā)生 除了JVM默認的三個ClassLoder以外,第三方可以編寫自己的類裝載器,以實現(xiàn)一些特殊的需求。類文件被裝載解析后,在JVM中都有一個對應的java.lang.Class對象,提供了類結(jié)構(gòu)信息的描述。數(shù)組,枚舉及基本數(shù)據(jù)類型,甚至void都擁有對應的Class對象。Class類沒有public的構(gòu)造方法,Class對象是在裝載類時由JVM通過調(diào)用類裝載器中的defineClass()方法自動構(gòu)造的。
雙親委托
一個類加載器查找class和resource時,是通過“委托模式”進行的,它首先判斷這個class是不是已經(jīng)加載成功,如果沒有的話它并不是自己進行查找,而是先通過父加載器,然后遞歸下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果沒有找到,則一級一級返回,最后到達自身去查找這些對象。這種機制就叫做雙親委托。
Java中ClassLoader的加載采用了雙親委托機制,采用雙親委托機制加載類的時候采用如下的幾個步驟:
當前ClassLoader首先從自己已經(jīng)加載的類中查詢是否此類已經(jīng)加載,如果已經(jīng)加載則直接返回原來已經(jīng)加載的類。
每個類加載器都有自己的加載緩存,當一個類被加載了以后就會放入緩存,等下次加載的時候就可以直接返回了。
當前classLoader的緩存中沒有找到被加載的類的時候,委托父類加載器去加載,父類加載器采用同樣的策略,首先查看自己的緩存,然后委托父類的父類去加載,一直到bootstrp ClassLoader.
當所有的父類加載器都沒有加載的時候,再由當前的類加載器加載,并將其放入它自己的緩存中,以便下次有加載請求的時候直接返回。
說到這里大家可能會想,Java為什么要采用這樣的委托機制?理解這個問題,我們引入另外一個關于Classloader的概念“命名空間”, 它是指要確定某一個類,需要類的全限定名以及加載此類的ClassLoader來共同確定。也就是說即使兩個類的全限定名是相同的,但是因為不同的 ClassLoader加載了此類,那么在JVM中它是不同的類。明白了命名空間以后,我們再來看看委托模型。采用了委托模型以后加大了不同的 ClassLoader的交互能力,比如上面說的,我們JDK本生提供的類庫,比如hashmap,linkedlist等等,這些類由bootstrp 類加載器加載了以后,無論你程序中有多少個類加載器,那么這些類其實都是可以共享的,這樣就避免了不同的類加載器加載了同樣名字的不同類以后造成混亂。
整個流程可以如下圖所示:

定義自已的ClassLoader
既然JVM已經(jīng)提供了默認的類加載器,為什么還要定義自已的類加載器呢?
因為Java中提供的默認ClassLoader,只加載指定目錄下的jar和class,如果我們想加載其它位置的類或jar時,比如:我要加載網(wǎng)絡上的一個class文件,通過動態(tài)加載到內(nèi)存之后,要調(diào)用這個類中的方法實現(xiàn)我的業(yè)務邏輯。在這樣的情況下,默認的ClassLoader就不能滿足我們的需求了,所以需要定義自己的ClassLoader。
定義自已的類加載器分為兩步:
1、繼承java.lang.ClassLoader
2、重寫父類的findClass方法
讀者可能在這里有疑問,父類有那么多方法,為什么偏偏只重寫findClass方法?
因為JDK已經(jīng)在loadClass方法中幫我們實現(xiàn)了ClassLoader搜索類的算法,當在loadClass方法中搜索不到類時,loadClass方法就會調(diào)用findClass方法來搜索類,所以我們只需重寫該方法即可。如沒有特殊的要求,一般不建議重寫loadClass搜索類的算法。
線程上下文類加載器
線程上下文類加載器(context class loader)是從 JDK 1.2 開始引入的。類 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用來獲取和設置線程的上下文類加載器。如果沒有通過 setContextClassLoader(ClassLoader cl)方法進行設置的話,線程將繼承其父線程的上下文類加載器。Java 應用運行的初始線程的上下文類加載器是系統(tǒng)類加載器。在線程中運行的代碼可以通過此類加載器來加載類和資源。
前面提到的類加載器的代理模式并不能解決 Java 應用開發(fā)中會遇到的類加載器的全部問題。Java 提供了很多服務提供者接口(Service Provider Interface,SPI),允許第三方為這些接口提供實現(xiàn)。常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的接口由 Java 核心庫來提供,如 JAXP 的 SPI 接口定義包含在 javax.xml.parsers包中。這些 SPI 的實現(xiàn)代碼很可能是作為 Java 應用所依賴的 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 核心庫的一部分,是由引導類加載器來加載的;SPI 實現(xiàn)的 Java 類一般是由系統(tǒng)類加載器來加載的。引導類加載器是無法找到 SPI 的實現(xiàn)類的,因為它只加載 Java 的核心庫。它也不能代理給系統(tǒng)類加載器,因為它是系統(tǒng)類加載器的祖先類加載器。也就是說,類加載器的代理模式無法解決這個問題。
線程上下文類加載器正好解決了這個問題。如果不做任何的設置,Java 應用的線程的上下文類加載器默認就是系統(tǒng)上下文類加載器。在 SPI 接口的代碼中使用線程上下文類加載器,就可以成功的加載到 SPI 實現(xiàn)的類。線程上下文類加載器在很多 SPI 的實現(xiàn)中都會用到。
類加載器與Web容器
對于運行在 Java EE容器中的 Web 應用來說,類加載器的實現(xiàn)方式與一般的 Java 應用有所不同。不同的 Web 容器的實現(xiàn)方式也會有所不同。以 Apache Tomcat 來說,每個 Web 應用都有一個對應的類加載器實例。該類加載器也使用代理模式,所不同的是它是首先嘗試去加載某個類,如果找不到再代理給父類加載器。這與一般類加載器的順序是相反的。這是 Java Servlet 規(guī)范中的推薦做法,其目的是使得 Web 應用自己的類的優(yōu)先級高于 Web 容器提供的類。這種代理模式的一個例外是:Java 核心庫的類是不在查找范圍之內(nèi)的。這也是為了保證 Java 核心庫的類型安全。
絕大多數(shù)情況下,Web 應用的開發(fā)人員不需要考慮與類加載器相關的細節(jié)。下面給出幾條簡單的原則:
(1)每個 Web 應用自己的 Java 類文件和使用的庫的 jar 包,分別放在 WEB-INF/classes和 WEB-INF/lib目錄下面。
?。?)多個應用共享的 Java 類文件和 jar 包,分別放在 Web 容器指定的由所有 Web 應用共享的目錄下面。
(3)當出現(xiàn)找不到類的錯誤時,檢查當前類的類加載器和當前線程的上下文類加載器是否正確。
類加載器與OSGi
OSGi是 Java 上的動態(tài)模塊系統(tǒng)。它為開發(fā)人員提供了面向服務和基于組件的運行環(huán)境,并提供標準的方式用來管理軟件的生命周期。OSGi 已經(jīng)被實現(xiàn)和部署在很多產(chǎn)品上,在開源社區(qū)也得到了廣泛的支持。Eclipse就是基于OSGi 技術來構(gòu)建的。
OSGi 中的每個模塊(bundle)都包含 Java 包和類。模塊可以聲明它所依賴的需要導入(import)的其它模塊的 Java 包和類(通過 Import-Package),也可以聲明導出(export)自己的包和類,供其它模塊使用(通過 Export-Package)。也就是說需要能夠隱藏和共享一個模塊中的某些 Java 包和類。這是通過 OSGi 特有的類加載器機制來實現(xiàn)的。OSGi 中的每個模塊都有對應的一個類加載器。它負責加載模塊自己包含的 Java 包和類。當它需要加載 Java 核心庫的類時(以 java開頭的包和類),它會代理給父類加載器(通常是啟動類加載器)來完成。當它需要加載所導入的 Java 類時,它會代理給導出此 Java 類的模塊來完成加載。模塊也可以顯式的聲明某些 Java 包和類,必須由父類加載器來加載。只需要設置系統(tǒng)屬性 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 在其模塊內(nèi)部查找類 com.bundleA.Sample并定義它,所得到的類 com.bundleA.Sample實例就可以被所有聲明導入了此類的模塊使用。對于以 java開頭的類,都是由父類加載器來加載的。如果聲明了系統(tǒng)屬性 org.osgi.framework.bootdelegation=com.example.core.*,那么對于包 com.example.core中的類,都是由父類加載器來完成的。
OSGi 模塊的這種類加載器結(jié)構(gòu),使得一個類的不同版本可以共存在 Java 虛擬機中,帶來了很大的靈活性。不過它的這種不同,也會給開發(fā)人員帶來一些麻煩,尤其當模塊需要使用第三方提供的庫的時候。下面提供幾條比較好的建議:
(1)如果一個類庫只有一個模塊使用,把該類庫的 jar 包放在模塊中,在 Bundle-ClassPath中指明即可。
?。?)如果一個類庫被多個模塊共用,可以為這個類庫單獨的創(chuàng)建一個模塊,把其它模塊需要用到的 Java 包聲明為導出的。其它模塊聲明導入這些類。
(3)如果類庫提供了 SPI 接口,并且利用線程上下文類加載器來加載 SPI 實現(xiàn)的 Java 類,有可能會找不到 Java 類。如果出現(xiàn)了 NoClassDefFoundError異常,首先檢查當前線程的上下文類加載器是否正確。通過 Thread.currentThread().getContextClassLoader()就可以得到該類加載器。該類加載器應該是該模塊對應的類加載器。如果不是的話,可以首先通過 class.getClassLoader()來得到模塊對應的類加載器,再通過 Thread.currentThread().setContextClassLoader()來設置當前線程的上下文類加載器。