Java ClassLoader深刻講解

【轉】http://www.blogjava.net/lhulcn618/archive/2006/05/25/48230.htmlhtml

【參考】http://ifeve.com/classloader/java

當JVM(Java虛擬機)啓動時,會造成三個類加載器組成的初始類加載器層次結構。web

bootstrap classloaderbootstrap

  |數組

extension classloader安全

  |服務器

system classloader網絡

①,當啓動一個JVM時,bootstrap classloader會加載java的核心類,例如:rt.jar中的類。bootstrap classloader是其餘classloader的parent,它是惟一一個沒有parent的classloaderapp

②,接下來是extension classloader,它以bootstrap classloader做爲parent,它用來從Java系統變量java.ext.dir中的jar包中加載類的;jvm

③,最後,也是最重要的一個就是開發者使用的system classpath classloader。它是extension classloader的child,它用來從Java系統變量java.class.path下面加載類,能夠經過-classpath來制定這個位置。

注意類加載器的體系並非「繼承」體系,而是一個「委派」體系。大多數類加載器首先會到本身的parent中查找類或者資源,若是找不到,纔會在本身的本地進行查找。事實上,類加載器被定義加載哪些在parent中沒法加載到的類,這樣在較高層級的類加載器上的類型可以被「賦值」爲較低類加載器加載的類型。

類加載器的委託行爲動機是爲了不相同的類被加載屢次。(回到1995年,Java的主要方向被放在Applet上,那時候網絡帶寬優先,因此程序中的類直到用時纔會被加載。可是事實上,Java在服務器端展現 了強勁的能力,可是服務器端要求類加載器可以反轉委派原則,也就是先加載本地的類,若是加載不到,再到parent中加載。)

JavaEE 的委派模型:

每一個方塊都是一個classloader,JavaEE規範推薦每一個模塊的classloader先加載本身classloader的內容,若是加載不到纔會到parent的classloader中嘗試加載。

反轉委派原則的緣由是應用服務器中所攜帶的類庫並非應用所但願的,也許不適合應用開發者,一個常見的例子就是log4j的以來在容器和不一樣的應用中都存在,可是他們的版本大都不一樣。

Tomcat的類加載順序(開啓了delegate模式)

 

在Tomcat中,莫仍的行爲是先嚐試在bootstrap classloader和extension classloader中進行類型加載,若是加載不到則在WebappLoader中進行加載,若是仍是找不到則在Common中進行查找。在Alibaba使用的Tomcat開啓了delegate模式,所以加載類型時是會以parent classloader優先。

 

一、bootstrap classloader

bootstrap classloader-引導(也稱爲原始)類加載器,它負責加載Java的核心類庫。在Sun的JVM中,在執行java的命令中使用-Xbootclasspath選項或使用-D選項制定sun.boot.class.path系統屬性值何以制定附加的類。這個加載器是很是特殊的,它實際上不是java.lang.ClassLoader的子類,而是由JVM自身實現的。你們能夠經過執行如下代碼來得到bootstrap classloader加載了哪些核心類庫:

import java.net.URL; public class Test { public static void main(String[] args) { URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs(); for(URL url : urls) { System.out.println(url.toExternalForm()); } } }

結果爲:

file:/C:/Program%20Files/Java/jre7/lib/resources.jar file:/C:/Program%20Files/Java/jre7/lib/rt.jar file:/C:/Program%20Files/Java/jre7/lib/sunrsasign.jar file:/C:/Program%20Files/Java/jre7/lib/jsse.jar file:/C:/Program%20Files/Java/jre7/lib/jce.jar file:/C:/Program%20Files/Java/jre7/lib/charsets.jar file:/C:/Program%20Files/Java/jre7/lib/jfr.jar file:/C:/Program%20Files/Java/jre7/classes

這時你們知道了爲何咱們不須要在系統屬性CLASSPATH中指定這些類庫了吧,由於JVM在啓動的時候就自動加載他們了。

二、extension classloader

extension classloader - 擴展類加載器,它負責加載JRE的擴展目錄(JAVA_HOME/jre/lib/ex或者由java.ext.dirs系統屬性指定的)中JAR的類包。這爲引入除Java核心類之外的新功能提供了一個標準機制。由於默認的擴展目錄對全部從同一個JRE中啓動的JVM都是通用的,因此放入這個目錄的JAR類包對全部的JVM和system classloader都是可見的。在這個實例上調用方法getParent()老是返回空值null,由於引導加載器bootstrap classloader不是一個真正的ClassLoader實例。因此當你們執行如下代碼時

System.out.println(System.getProperty("java.ext.dirs")); ClassLoader extensionClassLoader = ClassLoader.getSystemClassLoader().getParent(); System.out.println("the parent of extension classloader :" + extensionClassLoader.getParent());

 結果爲:

C:\Program Files\Java\jre7\lib\ext;C:\Windows\Sun\Java\lib\ext the parent of extension classloader :null

extension classloader 是system classloader的parent,而bootstrap classloader是extension classloader的parent,但它不是一個實際的classloader,因此爲null.

三、system classloader

system classloader - 系統(也稱爲應用)類加載器,它負責在JVM被啓動時,加載來自在命令java中的-classpath或者java.class.path系統屬性或者CLASSPATH操做系統屬性所指定的JAR類包和類路徑。

(.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar)

總能經過靜態方法ClassLoader.getSystemClassLoader()找到該類加載器。若是沒有特別指定,則用戶自定義的任何類加載器都將該類加載器做爲它的父加載器。執行如下代碼便可得到:

System.out.println(System.getProperty("java.class.path"));

輸出結果則爲用戶在系統屬性裏面設置的CLASSPATH。

四、ClassLoader的全盤負責機制和Cache機制

classloader加載類用的是全盤負責委託機制。所謂全盤負責,便是當一個classloader加載一個class的時候,這個Class所依賴的和引用的全部Class也由這個classloader負責載入(例如父類),除非是顯示的使用另一個classloader載入;委託機制則是先讓parent(父)類加載器尋找(而不是super,它與parent classloader類不是繼承關係),只有在parent找不到的時候才從本身的類路勁中去尋找。此外類加載還採用了cache機制,也就是若是cache中保存了這個Class就直接返回它,若是沒有才從文件中讀取和轉換成Class,並存入cache,這就是爲何咱們修改了Class可是必須從新啓動JVM才能生效的緣由。

每一個ClassLoader加載Class的過程:

①,檢測此Class是否載入過(即在cache中是否存在此Class),若是有到⑧,不然到②;

②,若是parent classloader不存在(沒有parent,那parent必定是bootstrap classloader),到④;

③,請求parent classloader載入,若是成功到⑧,不然到⑤;

④,請求JVM從bootstrap class中載入,若是成功到⑧;

⑤,尋找Class文件(從與此classloader相關的類路徑中尋找),若是找不到則到⑦;

⑥,從文件中載入Class,到⑧;

⑦,拋出ClassNotFoundException;

⑧,返回Class;

 其中⑤、⑥步咱們能夠經過覆蓋ClassLoader的findClass方法來實現本身的載入策略。甚至覆蓋loadClass方法來實現本身的載入過程。

五、類加載器的順序:

先是bootstrap classloader,而後是extension classloader,最後纔是system classloader。你們會發現加載的Class越重要的越在前面。這樣作的緣由是處於安全性的考慮,試想若是system classloader親自加載一個具備破壞性的「java.lang.System」類的後果吧。這種委託機制保證了用戶即便具備一個這樣的類,也把它加入到了類路徑中,可是它永遠不會被載入,由於這個類老是由bootstrap classloader來加載的。你們能夠執行一下如下的代碼:

System.out.println(System.class.getClassLoader()); 結果:null

結果是null,這就代表java.lang.System是由bootstrap classloader加載的,覺得bootstrap classloader不是一個真正的ClassLoader實例,而是由JVM實現的,正如前面已經說過的、

類 Bootstrap package com.sun.jdi; import com.sun.tools.jdi.VirtualMachineManagerImpl; public class Bootstrap { public Bootstrap() { } public static synchronized VirtualMachineManager virtualMachineManager() { return VirtualMachineManagerImpl.virtualMachineManager(); } }

 六、JVM創建類加載器的結構

下面就讓咱們來看看JVM是如何來爲咱們創建類加載器的結構的:

sum.misc.Launcher,顧名思義,當你執行java命令的時候,JVM會先使用bootstrap classloader載入並初始化一個Launcher,執行如下代碼:

System.out.println("The Launcher`s classloader is " + sun.misc.Launcher.class.getClassLoader()); 結果:The Launcher`s classloader is null

由於使用bootstrap classloader加載,因此classloader is null。

Launcher會根據系統和命令設定初始化好classloader結構,JVM就用它來得到extension classloader和system classloader並載入全部須要載入的Class,最後執行java命令指定的帶有靜態的main方法的Class。extension classloader其實是sun.misc.Launcher$ExtClassLoader類的一個實例。system classloader其實是sun.misc.Launcher$APPClassLoader類的一個實例。而且都是java.net.URLClassLoader的子類。

讓咱們來看看Laucher初始化的過程的部分代碼:

public class Launcher { public Launcher() { ExtClassLoader localExtClassLoader; try { //初始化extension classloader
            localExtClassLoader = ExtClassLoader.getExtClassLoader(); } catch (IOException localIOException1) { throw new InternalError("Could not create extension class loader"); } try { //初始化system classloader,parent是extension classl
            this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader); } catch (IOException localIOException2) { throw new InternalError("Could not create application class loader"); } //將system classloader設置成當前線程的context classloader(將在後面介紹)
        Thread.currentThread().setContextClassLoader(this.loader); String str = System.getProperty("java.security.manager"); if (str != null) { SecurityManager localSecurityManager = null; if (("".equals(str)) || ("default".equals(str))) { localSecurityManager = new SecurityManager(); } else { try { localSecurityManager = (SecurityManager) this.loader .loadClass(str).newInstance(); } catch (IllegalAccessException localIllegalAccessException) { } catch (InstantiationException localInstantiationException) { } catch (ClassNotFoundException localClassNotFoundException) { } catch (ClassCastException localClassCastException) { } } if (localSecurityManager != null) { System.setSecurityManager(localSecurityManager); } else { throw new InternalError("Could not create SecurityManager: "
                        + str); } } } ...... //返回system classloader
    public ClassLoader getClassLoader() { return this.loader; } }

再看一下extension classloader的部分代碼:(Launcher$ExtClassLoader內部類)

static class ExtClassLoader extends URLClassLoader { public static ExtClassLoader getExtClassLoader() throws IOException { File[] arrayOfFile = getExtDirs(); try { (ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction() { public Launcher.ExtClassLoader run() throws IOException { int i = this.val$dirs.length; for (int j = 0; j < i; j++) { MetaIndex.registerDirectory(this.val$dirs[j]); } return new Launcher.ExtClassLoader(this.val$dirs); } }); } catch (PrivilegedActionException localPrivilegedActionException) { throw ((IOException)localPrivilegedActionException.getException()); } } void addExtURL(URL paramURL) { super.addURL(paramURL); } public ExtClassLoader(File[] paramArrayOfFile) throws IOException { super(null, Launcher.factory); } private static File[] getExtDirs() { //得到系統屬性java.ext.dirs
        String str = System.getProperty("java.ext.dirs"); File[] arrayOfFile; if (str != null) { StringTokenizer localStringTokenizer = new StringTokenizer(str, File.pathSeparator); int i = localStringTokenizer.countTokens(); arrayOfFile = new File[i]; for (int j = 0; j < i; j++) { arrayOfFile[j] = new File(localStringTokenizer.nextToken()); } } else { arrayOfFile = new File[0]; } return arrayOfFile; } 
  ...
}

 再看system classloader的部分代碼:(Launcher$AppClassLoader內部類)

static class AppClassLoader extends URLClassLoader { public static ClassLoader getAppClassLoader(final ClassLoader paramClassLoader)    throws IOException { //得到系統屬性「java.class.path」
        String str = System.getProperty("java.class.path"); final File[] arrayOfFile = str == null ? new File[0] : Launcher.getClassPath(str); (ClassLoader)AccessController.doPrivileged(new PrivilegedAction() { public Launcher.AppClassLoader run() { URL[] arrayOfURL = this.val$s == null ? new URL[0] : Launcher.pathToURLs(arrayOfFile); return new Launcher.AppClassLoader(arrayOfURL, paramClassLoader); } }); } }

看了源代碼你們就清楚了吧,extension classloader是使用系統屬性「java.ext.dirs」設置類搜索路徑的,並無使用parent。system classloader是使用系統屬性「java.class.path」設置類搜索路徑的,而且有一個parent classloader。Launcher初始化extension classloader,system classloader,並將system classloader設置成爲context classloader,可是僅僅返回system classloader給JVM。

這裏怎麼又出來一個context classloader呢?它有什麼用?咱們在創建一個線程Thread的時候,能夠爲這個線程經過setContextClassLoader方法來制定一個合適的classloader做爲這個線程測context classloader,當此線程運行的時候,咱們能夠經過getContextClassLoader方法來得到此context classloader,就能夠用它來載入咱們所須要的Class,默認的是system classloader。利用這個特性,咱們能夠打破classloader的委託機制了。父classloader能夠得到當前線程的context classloader,而這個context classloader可使它的子classloader或者其餘的classloader,那麼父classloader就能夠從其得到所需的Class,這就打破了只能向父classloader請求的限制了。這個機制能夠知足當咱們的classpath是在運行時才肯定,並由定製的classloader加載的時候,由system classloader(即在jvm classpath中)加載的class能夠經過context classloader得到定製的classloader並載入特定的class(一般是抽象類和接口,定製的classloader中是其實現),例如web應用中的servlet就是這種機制加載的。

七、動態載入和更新

好了,如今咱們瞭解了classloader的結構和工做原理,那麼咱們如何實如今運行時的動態載入和更新呢?只要咱們可以動態改變類搜索路徑和清除classloader的cache中已經載入的Class就好了,有兩個方案。一是咱們繼承一個classloader,覆蓋loadclass方法,動態的尋找Class文件並使用defineClass方法;另外一個則很是簡單實用,只要從新使用一個新的類搜索路勁來new一個classloader就好了,這樣即更新了類搜索路徑以便來載入新的Class,也從新生成了一個空白的cache(固然,類搜索路徑不必定必須更改)。咱們幾乎不用作什麼工做,java.netURLClassLoader正是一個符合咱們要求的classloader,咱們能夠直接使用或者繼承它就能夠了!

Constructor and Description: URLClassLoader(URL[] urls) Constructs a new URLClassLoader for the specified URLs using the default delegation parent ClassLoader. URLClassLoader(URL[] urls, ClassLoader parent) Constructs a new URLClassLoader for the given URLs. URLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) Constructs a new URLClassLoader for the specified URLs, parent class loader, and URLStreamHandlerFactory.

其中URL[] urls就是咱們要設置的類搜索路徑,parent就是這個classloader的parent classloader,默認的是system classloader。

如今咱們可以動態的載入Class了,這樣咱們就能夠利用newInstance來得到一個Object。但咱們若是將此Object造型呢?能夠將此Object造型成它本省的Class嗎?

首先讓咱們來分析一下java源文件的編譯,運行吧!javac命令是調用「JAVA_HOM/lib/tools.jar」中的「com.sun.tools.javac.Main」的compile方法來編譯:

public static int compile(String as[]); public static int compile(String as[], PrintWriter printwriter);

返回0表示編譯成功,字符串數組as則是咱們使用javac命令編譯時的參數,以空格劃分。例如:

javac -classpath c:\foo\bar.jar;. -d c:\Some.java

則字符串數組as爲{"-classpath","c:\\foo\\bar.jar;.","-d","c:\\","c:\\Some.java"},若是帶有PrintWriter參數,則會把編譯信息輸出到這個指定的printwriter中。默認的輸出是System.err。

其中Main是由JVM使用Launcher初始化system classloader載入的,根據全盤負責原則,編譯器在解析這個java源文件時所發現的它所依賴和引用的全部Class也將由system classloader載入,若是system classloader不能載入某個Class,編譯器將拋出一個「cannot resolve symbol」錯誤。

因此首先變異就不會經過,也就是編譯器沒法編譯一個引用了在CLASSPATH中位置Class的java源文件,而因爲拼寫錯誤或者沒有吧所需類庫放到CLASSPATH中,你們必定常常看到這個「cannot resolve symbol」這個編譯錯誤吧!

其次,就是咱們把這個Class放到編譯路徑中,成功的進行了編譯,而後在運行的時候不把它放到CLASSPATH中而利用咱們本身的classloader來動態載入這個Class,這時候也會出現「java.lang.NoClassDefFoundError」的違例,爲何呢?

咱們再來分析一下,首先調用這個造型語句的可執行的Class必定是由JVM使用Laucher初始化的system classloader載入的,根據全盤負責原則,但咱們進行造型的時候,JVM也會使用system classloader來蠶食載入這個Class來對實例進行造型,天然在system classloader尋找不到這個Class時就會拋出「java.lang.NoClassDefFoundError」的違例。

OK,如今咱們總結一下,java文件的變異和Class的載入執行,都是適應Launcher初始化system classloader做爲類載入器的,咱們沒法動態的改變system classloader,更沒法讓JVM使用咱們本身的classloader來替換system classloader,根據全盤負責原則,就限制了編譯和運行時,咱們沒法直接顯示的使用system classloader尋找不到的Class,即咱們只能使用Java核心類庫,擴展類庫和CLASSPATH中的類庫中的Class。

還不死心!再嘗試一下這種狀況:咱們把這個Class也放到CLASSPATH中,讓system classloader可以識別和載入。而後咱們經過本身的classloader來從指定的class文件中載入這個Class(不可以委託parent載入,由於這樣會被system classloader從CLASSPATH中將其載入)。而後實例化一個Object,並造型成這個Class,這樣JVM也是別這個Class(由於system classloader可以從CLASSPATH中定位和載入這個Class),載入的也不是CLASSPATH中的這個Class,而是從CLASSPATH外動態載入的,這樣總行了吧,十分不幸的是,這時會出現「java.lang.ClassCastException」違例。

爲何呢?咱們也來分析一下,不錯,咱們雖然從CLASSPATH外使用咱們本身的classload動態載入了這個Class,但將它的實例造型的時候是JVM使用system classloader來在此載入這個Class,而咱們使用的是本身classloader並載入的這個Class的一個實力造型。你們發現什麼問題了嗎?也就是咱們嘗試將從一個classloader載入的Class的一個實例造型是另一個classloader載入的Class,雖然兩個Class的名字同樣,甚至是從同一個class文件中載入。但不幸的是JVM卻認爲這兩個Class是不一樣的,即JVM認爲不一樣的classloader載入的相同的名字的Class是不一樣的(即便是從同一個class文件中載入的)!這樣作的緣由我想大概也是主要出於安全性考慮,這樣就保證全部的和興Java類都是system classloader載入的,咱們沒法用本身的classloader載入相同名字的Class的實例來替換他們的實例。

到這裏,應該就想到了該如何動態載入咱們的Class,實例化,造型並調用了。

那就是利用面向對象的基本特性之一的多態性。咱們把咱們動態載入的Class的實例造型成它的一個system classloader所能識別的父類就好了,這是爲何呢,咱們仍是要再分析一下。當咱們用咱們本身的classloader來動態載入這個Class的時候,發現它有一個父類Class,在載入它以前JVM必須先載入這個父類的Class,這個父類Class是system classloader所能識別的,根據委託機制,它將由system classloader載入,而後咱們的classloader再載入這個子類Class,建立一個實例,造型爲這個父類Class,注意了,造型成這個父類Class的時候(向上轉型)是面向對象的java語言所容許的而且JVM也是支持的,JVM就是用system classloader再次載入這個父類Class,而後將此子類實例造型爲這個父類的Class。你們能夠從這個過程當中發現這個父類Class都是有system classloader載入的,也就是同一個class loader載入的同一個Class,因此造型的時候不會出現任何異常。而根據多態性,調用這個父類的方法時,真正執行的是這個子類Class(非父類Class)的覆蓋了父類的方法。這些方法中也能夠引用system classloader不能識別的Class,由於根據全盤負責原則,只要載入這個Class的classloader即咱們本身定義的classloader可以定位和載入這些Class就好了。(即牛人說的Java文件在生成class文件的編譯過程當中,代碼就已經寫好了的)

這樣咱們就能夠事先定義好一組接口或者基類並放入CLASSPATH中,而後在執行的時候動態的載入實現或者繼承了這些接口或基類的子類。好比Servlet,web application server可以載入任何繼承了Servlet的Class並正確的執行它們,無論它實際的Class是什麼,就是都把它們實例化稱爲一個Servlet Class,而後執行Servlet init doPost doGet destroy..等方法的,而無論這個Servlet是從web-inf/lib和web-inf/classes下由system classloader的子classloader(即定製的classloader)動態載入的。說了這麼多但願你們都明白了。再applet,ejb容器中,都是採用了這種機制。

對於以上各類狀況,但願你們實際編寫一些example來實驗一下。

最後說點別的,classloader雖然稱爲類加載器,但並不意味着只能用來加載Class,咱們還能夠利用它來得到圖片,音頻文件等資源的URL,固然,這些資源必須在CLASSPATH中的jar類庫或者目錄下。在API的doc中關於ClassLoader的兩個尋找資源和Class的方法描述吧:

public URL getResource(String name) 用指定的名字來查找資源,一個資源是一些可以被class代碼訪問的在某種程度上依賴於代碼位置的數據(圖片,音頻,文本等等)。一個資源的名字是以'/'號分隔肯定資源的路徑名的。這個方法將先請求parent classloader搜索資源,若是沒有parent,則會在內置在虛擬機中的classloader(即bootstrap classloader)的路徑中搜索。若是失敗,這個方法將調用findResource(String)來尋找資源。 public static URL getSystemResource(String name) 從用來載入類的搜索路徑中查找一個指定名字的資源。這個方法使用system class loader來定位資源。即至關於ClassLoader.getSystemClassLoader().getResource(name)。

例如:

System.out.println(ClassLoader.getSystemResource("java/lang/String.class")); 結果:jar:file:/C:/Program%20Files/Java/jre7/lib/rt.jar!/java/lang/String.class

代表String.class文件在rt。jar的java/lang目錄中。

所以咱們能夠將圖片等資源隨同Class一同打包到jar類庫中(固然,也能夠單獨打包這些資源),並添加他們到class loader的搜索路徑中,咱們就能夠無需關心這些資源的具體位置,讓class loader來幫咱們尋找了。

相關文章
相關標籤/搜索