A.Bootstrap Loader
AppClassLoader在SUN官方文件中,常常被稱為系統(tǒng)載入器(System Loader),一個應(yīng)用啟動,最后的步驟就是由AppClassLoader載入我們的XXXMain.class。整個流程如下圖:
這個Bootstrap Loader----ExtClassLoader---AppClassLoader,就是我們的類載入體系,
public class ClassLoaderTest {
public static void main(String args[]) throws Exception{
ClassLoader cl=ClassLoaderTest.class.getClassLoader();
System.out.println("========載入體系======");
System.out.println(cl);
System.out.println(cl.getParent());
System.out.println(cl.getParent().getParent());
System.out.println("========ClassLoader======");
System.out.println(cl.getClass().getClassLoader());
System.out.println(cl.getParent().getClass().getClassLoader());
}
}
========載入體系======
sun.misc.Launcher$AppClassLoader@19821f
sun.misc.Launcher$ExtClassLoader@addbf1
null
========ClassLoader======
null
null
ClassLoader和Parent
除了boot,其他ClassLoader本身也是java類,也需要有ClassLoader
這里就可以看出,ClassLoader本身也是Java寫的,所以載入他們,也使用getClassLoader,和Parent是誰沒關(guān)系,
B.3種ClassLoader的搜索路徑
App和Ext都是URLClassLoader的子類,所以他們實(shí)際也需要一個URL作為參考,去載入類,App所參考的URL是從系統(tǒng)參數(shù)java.class.path取出來的,這個參數(shù),可以在我們執(zhí)行java.exe時,使用-cp或者-classpath或者CLASSPATH環(huán)境變量指定:
String s=System.getProperty("java.class.path");
System.out.println(s);
classpath,代表的是當(dāng)前類目錄(或者說當(dāng)前項(xiàng)目對應(yīng)的class目錄),已經(jīng)所引用的jar文件。使用-cp或者-classpath可以改變,相對于系統(tǒng)的CLASSPATH,優(yōu)先使用-cp或者-classpath。
EXT也差不多,他參考的是系統(tǒng)參數(shù)java.ext.dirs:
s=System.getProperty("java.ext.dirs");
System.out.println(s);
輸出:
D:\PF\Java\jre6\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
它會指向java.exe所選擇的JRE所在位置下的lib\ext目錄,可以通過java.ext.dirs來改變。
Bootstrap Loader參考的是系統(tǒng)參數(shù),sun.boot.class.path:
s=System.getProperty("sun.boot.class.path");
System.out.println(s);
通過結(jié)果可以看到,他包括了jre的lib目錄的一些jar以及classes目錄。
委派模型
parent的作用,其實(shí)就是為了實(shí)現(xiàn)委派模型。所謂的委派模型,就是,類載入器當(dāng)有類需要載入時,會先讓parent采用他的搜尋路徑去查找類,如果parent找不到,那么才由自己去找。
通過這個委派模型,解釋下下面的代碼(順便說下,為什么如果能引用到包,那就會有不同的效果):
URL url=new URL("file:/C:/jar1.jar");
URLClassLoader ucl=new URLClassLoader(new URL[]{url});
Class c=ucl.loadClass("bhsc.hello.TestAction");
ActionInterface a1=(ActionInterface)c.newInstance();
a1.action();
//,Thread.currentThread().getContextClassLoader()
URL url1=new URL("file:/C:/jar1.jar");
URLClassLoader u2=new URLClassLoader(new URL[]{url1});
Class c1=u2.loadClass("bhsc.hello.TestAction");
c.newInstance();
ActionInterface a2=(ActionInterface)c1.newInstance();
a2.action();
System.out.println(ClassLoaderTest.class.getClassLoader());
System.out.println(ActionInterface.class.getClassLoader());
System.out.println(ucl.getClass().getClassLoader());
System.out.println(c.getClass().getClassLoader());
System.out.println(c.getClassLoader());
System.out.println(a1.getClass().getClassLoader());
System.out.println("==============================");
System.out.println(u2.getClass().getClassLoader());
System.out.println(c1.getClass().getClassLoader());
System.out.println(c1.getClassLoader());
System.out.println(a2.getClass().getClassLoader());
結(jié)果:
static init
TestAction
static init
TestAction
sun.misc.Launcher$AppClassLoader@19821fsun.misc.Launcher$AppClassLoader@19821f
null
null
java.net.URLClassLoader@c17164
java.net.URLClassLoader@c17164
==============================
null
null
java.net.URLClassLoader@ca0b6
java.net.URLClassLoader@ca0b6這個程序中,java虛擬機(jī)一共產(chǎn)生5個ClassLoader:
首選需要載入ClassLoaderTest,所以App需要載入他,他會先向Ext請求,Ext再向Boot請求,Boot沒有找到,Ext也沒有找到,最后App在class目錄下找到了。類中的URL和URLClassLoader,默認(rèn)使用的是當(dāng)前類本身所使用的載入器,也就是App(私有方法ClassLoader.getCallerClassLoader,new的時候自己使用的),所以還是按照App—Ext—Boot請求,Boot在jre的lib的rj.jar下找到了,所以由Boot載入。
URLLoader請求bhsc.hello.TestAction,首先他通過Parent(App),Parent的Parent(Ext),Parent的Parent的Parent(Boot),最后由App載入ActionInterface由URL載入實(shí)現(xiàn)類。所以如果TestAction再App能找到的地方,那么也還是不會由URLLoader去載入了。
造成的一個問題(線程上下文類加載器)
Java的這個類別載入體系,也照成了一個問題,那就是載入器可以看到Parent所能載入的類別,但是反過來卻不是這樣。比如,ClassA本身在Boot或者Ext下找到,那么他里面new出來的一些類也就只能用Boot或者Ext去查找了(不會低一個級別),所以有些明明App可以找到的人,卻找不到了。
比如JDBC API,他有實(shí)現(xiàn)的driven部分(mysql/sql server),我們的JDBC API都是由Boot或者Ext來載入的,但是JDBC driver卻是由Ext或者App來載入,那么就有可能找不到driver了。在Java領(lǐng)域中,其實(shí)只要分成這種Api+SPI(Service Provide Interface,特定廠商提供)的,都會遇到此問題。
這時候就應(yīng)該使用線程上下文類加載器。
. 通常當(dāng)你需要動態(tài)加載資源的時候 , 你至少有三個 ClassLoader 可以選擇 :
1.系統(tǒng)類加載器或叫作應(yīng)用類加載器 (system classloader or application classloader)
2.當(dāng)前類加載器
3.當(dāng)前線程類加載器
(當(dāng)前類加載器是為了證明了java類加載器中的名稱空間是唯一的, 不會相互干擾.
即在一般情況下, 保證同一個類中所關(guān)聯(lián)的其他類都是由當(dāng)前類的類加載器所加載的.。
當(dāng)前線程類加載器是為了拋棄雙親委派加載鏈模式, 使用線程上下文里的類加載器加載類.因?yàn)橐恍?biāo)準(zhǔn)的API是boot加載器,但是,boot加載器并不能加載到第三方實(shí)現(xiàn)的jar,
如果不是多線程環(huán)境,基本可以認(rèn)為兩者得出的結(jié)果相同,不過多線程已經(jīng)越來越普及,所以還是用線程加載器的好
)
前面的第一個App是最不應(yīng)該使用的,我們前面說過,他參考的是環(huán)境變量classpath,在一個main函數(shù)下執(zhí)行的時候,classpath通常是class目錄以及所以來的jar包,可以作為一個web應(yīng)用執(zhí)行的時候,確不一樣,比如一個簡單的web應(yīng)用,在tomcat下打印出來的卻是:
E:\apache-tomcat-6.0.29\apache-tomcat-6.0.29\bin\bootstrap.jar,僅僅是這么一個jar包
我們將上面的程序,放到tomcat中,會包找不到這個類的異常,這是因?yàn)?,我們將?yīng)用放到tomcat中,ActionInterface默認(rèn)將使用tomcat的StandardClassLoader來加載,而我們的bhsc.hello.TestAction是需要使用URLClassLoader來加載的,他的父類是ActionInterface,可是在URL這個類加載器下(file:/C:/jar1.jar)是找不到ActionInterface,所以他URLLoader會請求App以及Ext和Boot,就都找不到了。我們試用URLClassLoader的第二個構(gòu)造函數(shù):
URLClassLoader myClassLoader = new URLClassLoader(new URL[] {url} , Thread.currentThread().getContextClassLoader());
這里傳入了一個父類加載器,看一下文檔,解釋是:
為給定的 URL 構(gòu)造新 URLClassLoader。首先在指定的父類加載器中搜索 URL,然后按照為類和資源指定的順序搜索 URL。這里假定任何以 '/' 結(jié)束的 URL 都是指向目錄的。如果不是以該字符結(jié)束,則認(rèn)為該 URL 指向一個將根據(jù)需要下載和打開的 JAR 文件。
所以會現(xiàn)在當(dāng)前線程加載類,也就是StandardClassLoader找尋找,就找到了ActionInterface,并且,類型也是匹配的。
常見的 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() 方法用來生成一個新的 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)類加載器的祖先類加載器。也就是說,類加載器的代理模式無法解決這個問題。
線程上下文類加載器正好解決了這個問題。如果不做任何的設(shè)置,Java 應(yīng)用的線程的上下文類加載器默認(rèn)就是系統(tǒng)上下文類加載器。在 SPI 接口的代碼中使用線程上下文類加載器,就可以成功的加載到 SPI 實(shí)現(xiàn)的類。線程上下文類加載器在很多 SPI 的實(shí)現(xiàn)中都會用到。
上下文類加載器為同樣在J2SE中引入的類加載代理機(jī)制提供了后門。通常JVM中的類加載器是按照層次結(jié)構(gòu)組織的,目的是每個類加載器(除了啟動整個JVM的原初類加載器)都有一個父類加載器。當(dāng)類加載請求到來時,類加載器通常首先將請求代理給父類加載器。只有當(dāng)父類加載器失敗后,它才試圖按照自己的算法查找并定義當(dāng)前類。讓核心JNDI類使用線程上下文類加載器,從而有效的打通類加載器層次結(jié)構(gòu),逆著代理機(jī)制的方向使用類加載器。
一個經(jīng)典的實(shí)例說明
我們看看下面的代碼:
Java代碼
package java.lang;
public class String {
public static void main(String[] args){
}
}
大家發(fā)現(xiàn)什么不同了嗎?對了,我們寫了一個與JDK中String一模一樣的類,連包java.lang都一樣,唯一不同的是我們自定義的String類有一個main函數(shù)。我們來運(yùn)行一下:
java.lang.NoSuchMethodError: main
Exception in thread "main"
這是為什么? 我們的String類不是明明有main方法嗎?
其實(shí)聯(lián)系我們上面講到的雙親委托模型,我們就能解釋這個問題了。
運(yùn)行這段代碼,JVM會首先創(chuàng)建一個自定義類加載器,不妨叫做AppClassLoader,并把這個加載器鏈接到委托鏈中:AppClassLoader -> ExtClassLoader -> BootstrapLoader。
然后AppClassLoader會將加載java.lang.String的請求委托給ExtClassLoader,而 ExtClassLoader又會委托給最后的啟動類加載器BootstrapLoader。
啟動類加載器BootstrapLoader只能加載JAVA_HOME\jre\lib中的class類(即J2SE API),問題是標(biāo)準(zhǔn)API中確實(shí)有一個java.lang.String(注意,這個類和我們自定義的類是完全兩個類)。BootstrapLoader以為找到了這個類,毫不猶豫的加載了j2se api中的java.lang.String。
最后出現(xiàn)上面的加載錯誤(注意不是異常,是錯誤,JVM退出),因?yàn)锳PI中的String類是沒有main方法的。
載入器的作用
載入器是一個非常復(fù)雜的部分,這么做的好處之一就是安全性:
假設(shè)我們利用URLClassLoader到網(wǎng)絡(luò)上任何地方下載其他類,URLLoader都不可能下載App、Ext或者Boot可以找到的同名類,因?yàn)椋钜馄茐恼?,沒有機(jī)會植入有問題的代碼到我們的電腦中。
第2,類別載入器,無法看到其他相同層級的類別載入器可以載入的類別,比如,
www.sun.com載入的類,無法看到
www.xxx.com載入的類,這意味著,不同的類別載入器可以載入完全相同的類別之外,也排除了誤用或者惡意使用別人代碼的機(jī)會。
第三,和Java的類判斷有關(guān)
Java 虛擬機(jī)是如何判定兩個 Java 類是相同的。Java 虛擬機(jī)不僅要看類的全名是否相同,還要看加載此類的類加載器是否一樣。只有兩者都相同的情況,才認(rèn)為兩個類是相同的。即便是同樣的字節(jié)代碼,被不同的類加載器加載之后所得到的類,也是不同的。比如一個 Java 類 com.example.Sample,編譯之后生成了字節(jié)代碼文件 Sample.class。兩個不同的類加載器 ClassLoaderA 和 ClassLoaderB 分別讀取了這個 Sample.class 文件,并定義出兩個 java.lang.Class 類的實(shí)例來表示這個類。這兩個實(shí)例是不相同的。對于 Java 虛擬機(jī)來說,它們是不同的類。試圖對這兩個類的對象進(jìn)行相互賦值,會拋出運(yùn)行時異常 ClassCastException。
委派模型保證了 Java 核心庫的類型安全。所有 Java 應(yīng)用都至少需要引用 java.lang.Object 類,也就是說在運(yùn)行的時候,java.lang.Object 這個類需要被加載到 Java 虛擬機(jī)中。如果這個加載過程由 Java 應(yīng)用自己的類加載器來完成的話,很可能就存在多個版本的 java.lang.Object 類,而且這些類之間是不兼容的。通過代理模式,對于 Java 核心庫的類的加載工作由引導(dǎo)類加載器來統(tǒng)一完成,保證了 Java 應(yīng)用所使用的都是同一個版本的 Java 核心庫的類,是互相兼容的。
再參考:
http://www.javaeye.com/topic/136427 重溫java之classloader體系結(jié)構(gòu)(含hotswap)
走出ClassLoader迷局(譯)http://blog.csdn.net/adverse/archive/2009/03/22/4014870.aspx利用URLClassLoader加載class到當(dāng)前線程
深入了解Java ClassLoader、Bytecode 、ASM、cglib