1. 程式人生 > >java 中ClassLoader 的載入順序

java 中ClassLoader 的載入順序

原文引自:http://www.blogjava.net/lhulcn618/archive/2006/05/25/48230.html

當JVM(Java虛擬機器)啟動時,會形成由三個類載入器組成的初始類載入器層次結構:


       bootstrap classloader
                |
       extension classloader
                |
       system classloader

bootstrapclassloader-引導(也稱為原始)類載入器,它負責載入Java的核心類。在Sun的JVM中,在執行java的命令中使用-Xbootclasspath選項或使用- D選項指定sun.boot.class.path系統屬性值可以指定附加的類。這個載入器的是非常特殊的,它實際上不是java.lang.ClassLoader的子類,而是由JVM自身實現的。大家可以通過執行以下程式碼來獲得bootstrapclassloader載入了那些核心類庫:
   URL[] urls=sun.misc.Launcher.getBootstrapClassPath().getURLs();
   for (int i = 0; i < urls.length; i++) {
     System.out.println(urls.toExternalform());
   }
在我的計算機上的結果為:
檔案:/C:/j2sdk1.4.1_01/jre/lib/endorsed/dom.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/endorsed/sax.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xalan-2.3.1.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xercesImpl-2.0.0.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xml-apis.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xsltc.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/rt.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/i18n.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/sunrsasign.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/jsse.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/jce.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/charsets.jar
檔案:/C:/j2sdk1.4.1_01/jre/classes
這時大家知道了為什麼我們不需要在系統屬性CLASSPATH中指定這些類庫了吧,因為JVM在啟動的時候就自動載入它們了。

extensionclassloader-擴充套件類載入器,它負責載入JRE的擴充套件目錄(JAVA_HOME/jre/lib/ext或者由java.ext.dirs系統屬性指定的)中JAR的類包。這為引入除Java核心類以外的新功能提供了一個標準機制。因為預設的擴充套件目錄對所有從同一個JRE中啟動的JVM都是通用的,所以放入這個目錄的JAR類包對所有的JVM和systemclassloader都是可見的。在這個例項上呼叫方法getParent()總是返回空值null,因為引導載入器bootstrapclassloader不是一個真正的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:/j2sdk1.4.1_01/jre/lib/ext
the parent of extension classloader : null
extensionclassloader是system classloader的parent,而bootstrap classloader是extensionclassloader的parent,但它不是一個實際的classloader,所以為null。

systemclassloader-系統(也稱為應用)類載入器,它負責在JVM被啟動時,載入來自在命令java中的-classpath或者java.class.path系統屬性或者CLASSPATH作業系統屬性所指定的JAR類包和類路徑。總能通過靜態方法ClassLoader.getSystemClassLoader()找到該類載入器。如果沒有特別指定,則使用者自定義的任何類載入器都將該類載入器作為它的父載入器。執行以下程式碼即可獲得:
   System.out.println(System.getProperty("java.class.path"));
輸出結果則為使用者在系統屬性裡面設定的CLASSPATH。
classloader載入類用的是全盤負責委託機制。所謂全盤負責,即是當一個classloader載入一個Class的時候,這個Class所依賴的和引用的所有Class也由這個classloader負責載入,除非是顯式的使用另外一個classloader載入;委託機制則是先讓parent(父)類載入器(而不是super,它與parentclassloader類不是繼承關係)尋找,只有在parent找不到的時候才從自己的類路徑中去尋找。此外類載入還採用了cache機制,也就是如果cache中儲存了這個Class就直接返回它,如果沒有才從檔案中讀取和轉換成Class,並存入cache,這就是為什麼我們修改了Class但是必須重新啟動JVM才能生效的原因。


每個ClassLoader載入Class的過程是:
1.檢測此Class是否載入過(即在cache中是否有此Class),如果有到8,如果沒有到2
2.如果parent classloader不存在(沒有parent,那parent一定是bootstrap classloader了),到4
3.請求parent classloader載入,如果成功到8,不成功到5
4.請求jvm從bootstrap classloader中載入,如果成功到8
5.尋找Class檔案(從與此classloader相關的類路徑中尋找)。如果找不到則到7.
6.從檔案中載入Class,到8.
7.丟擲ClassNotFoundException.
8.返回Class.

其中5.6步我們可以通過覆蓋ClassLoader的findClass方法來實現自己的載入策略。甚至覆蓋loadClass方法來實現自己的載入過程。

類載入器的順序是:
先是bootstrap classloader,然後是extension classloader,最後才是systemclassloader。大家會發現載入的Class越是重要的越在靠前面。這樣做的原因是出於安全性的考慮,試想如果systemclassloader“親自”載入了一個具有破壞性的“java.lang.System”類的後果吧。這種委託機制保證了使用者即使具有一個這樣的類,也把它加入到了類路徑中,但是它永遠不會被載入,因為這個類總是由bootstrap classloader來載入的。大家可以執行一下以下的程式碼:
   System.out.println(System.class.getClassLoader());
將會看到結果是null,這就表明java.lang.System是由bootstrap classloader載入的,因為bootstrap classloader不是一個真正的ClassLoader例項,而是由JVM實現的,正如前面已經說過的。

下面就讓我們來看看JVM是如何來為我們來建立類載入器的結構的:
sun.misc.Launcher,顧名思義,當你執行java命令的時候,JVM會先使用bootstrap classloader載入並初始化一個Launcher,執行下來程式碼:
  System.out.println("the Launcher's classloader is "+sun.misc.Launcher.getLauncher().getClass().getClassLoader());
結果為:
  the Launcher's classloader is null (因為是用bootstrap classloader載入,所以class loader為null)
Launcher會根據系統和命令設定初始化好class loader結構,JVM就用它來獲得extension classloader和systemclassloader,並載入所有的需要載入的Class,最後執行java命令指定的帶有靜態的main方法的Class。extensionclassloader實際上是sun.misc.Launcher$ExtClassLoader類的一個例項,systemclassloader實際上是sun.misc.Launcher$AppClassLoader類的一個例項。並且都是java.net.URLClassLoader的子類。

讓我們來看看Launcher初試化的過程的部分程式碼。

Launcher的部分程式碼:
public class Launcher  {
   public Launcher() {
       ExtClassLoader extclassloader;
       try {
           //初始化extension classloader
           extclassloader = ExtClassLoader.getExtClassLoader();
       } catch(IOException ioexception) {
           throw new InternalError("Could not create extension class loader");
       }
       try {
           //初始化system classloader,parent是extension classloader
           loader = AppClassLoader.getAppClassLoader(extclassloader);
       } catch(IOException ioexception1) {
           throw new InternalError("Could not create application class loader");
       }
       //將system classloader設定成當前執行緒的context classloader(將在後面加以介紹)
       Thread.currentThread().setContextClassLoader(loader);
       ......
   }
   public ClassLoader getClassLoader() {
       //返回system classloader
       return loader;
   }
}

extension classloader的部分程式碼:
static class Launcher$ExtClassLoader extends URLClassLoader {

   public static Launcher$ExtClassLoader getExtClassLoader()
       throws IOException
   {
       File afile[] = getExtDirs();
       return (Launcher$ExtClassLoader)AccessController.doPrivileged(new Launcher$1(afile));
   }
  private static File[] getExtDirs() {
       //獲得系統屬性“java.ext.dirs”
       String s = System.getProperty("java.ext.dirs");
       File afile[];
       if(s != null) {
           StringTokenizer stringtokenizer = new StringTokenizer(s, File.pathSeparator);
           int i = stringtokenizer.countTokens();
           afile = new File;
           for(int j = 0; j < i; j++)
               afile[j] = new File(stringtokenizer.nextToken());

       } else {
           afile = new File[0];
       }
       return afile;
   }
}

system classloader的部分程式碼:
static class Launcher$AppClassLoader extends URLClassLoader
{

   public static ClassLoader getAppClassLoader(ClassLoader classloader)
       throws IOException
   {
       //獲得系統屬性“java.class.path”
       String s = System.getProperty("java.class.path");
       File afile[] = s != null ? Launcher.access$200(s) : new File[0];
       return (Launcher$AppClassLoader)AccessController.doPrivileged(new Launcher$2(s, afile, classloader));
   }
}

看了原始碼大家就清楚了吧,extensionclassloader是使用系統屬性“java.ext.dirs”設定類搜尋路徑的,並且沒有parent。systemclassloader是使用系統屬性“java.class.path”設定類搜尋路徑的,並且有一個parentclassloader。Launcher初始化extension classloader,systemclassloader,並將system classloader設定成為context classloader,但是僅僅返回systemclassloader給JVM。

  這裡怎麼又出來一個contextclassloader呢?它有什麼用呢?我們在建立一個執行緒Thread的時候,可以為這個執行緒通過setContextClassLoader方法來指定一個合適的classloader作為這個執行緒的contextclassloader,當此執行緒執行的時候,我們可以通過getContextClassLoader方法來獲得此contextclassloader,就可以用它來載入我們所需要的Class。預設的是systemclassloader。利用這個特性,我們可以“打破”classloader委託機制了,父classloader可以獲得當前執行緒的contextclassloader,而這個contextclassloader可以是它的子classloader或者其他的classloader,那麼父classloader就可以從其獲得所需的Class,這就打破了只能向父classloader請求的限制了。這個機制可以滿足當我們的classpath是在執行時才確定,並由定製的classloader載入的時候,由system classloader(即在jvmclasspath中)載入的class可以通過contextclassloader獲得定製的classloader並載入入特定的class(通常是抽象類和介面,定製的classloader中是其實現),例如web應用中的servlet就是用這種機制載入的.


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

這是j2se1.4 API的doc中URLClassLoader的兩個構造器的描述:
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.
其中URL[] urls就是我們要設定的類搜尋路徑,parent就是這個classloader的parent classloader,預設的是system classloader。


好,現在我們能夠動態的載入Class了,這樣我們就可以利用newInstance方法來獲得一個Object。但我們如何將此Object造型呢?可以將此Object造型成它本身的Class嗎?

首先讓我們來分析一下java原始檔的編譯,執行吧!javac命令是呼叫“JAVA_HOME/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:/ c:/Some.java
則字串陣列as為{"-classpath","c://foo//bar.jar;.","-d","c://","c://Some.java"},如果帶有PrintWriter引數,則會把編譯資訊出到這個指定的printWriter中。預設的輸出是System.err。

其中Main是由JVM使用Launcher初始化的systemclassloader載入的,根據全盤負責原則,編譯器在解析這個java原始檔時所發現的它所依賴和引用的所有Class也將由systemclassloader載入,如果system classloader不能載入某個Class時,編譯器將丟擲一個“cannot resolvesymbol”錯誤。

所以首先編譯就通不過,也就是編譯器無法編譯一個引用了不在CLASSPATH中的未知Class的java原始檔,而由於拼寫錯誤或者沒有把所需類庫放到CLASSPATH中,大家一定經常看到這個“cannot resolve symbol”這個編譯錯誤吧!

其次,就是我們把這個Class放到編譯路徑中,成功的進行了編譯,然後在執行的時候不把它放入到CLASSPATH中而利用我們自己的classloader來動態載入這個Class,這時候也會出現“java.lang.NoClassDefFoundError”的違例,為什麼呢?

我們再來分析一下,首先呼叫這個造型語句的可執行的Class一定是由JVM使用Launcher初始化的systemclassloader載入的,根據全盤負責原則,當我們進行造型的時候,JVM也會使用systemclassloader來嘗試載入這個Class來對例項進行造型,自然在systemclassloader尋找不到這個Class時就會丟擲“java.lang.NoClassDefFoundError”的違例。

OK,現在讓我們來總結一下,java檔案的編譯和Class的載入執行,都是使用Launcher初始化的systemclassloader作為類載入器的,我們無法動態的改變systemclassloader,更無法讓JVM使用我們自己的classloader來替換systemclassloader,根據全盤負責原則,就限制了編譯和執行時,我們無法直接顯式的使用一個systemclassloader尋找不到的Class,即我們只能使用Java核心類庫,擴充套件類庫和CLASSPATH中的類庫中的Class。

還不死心!再嘗試一下這種情況,我們把這個Class也放入到CLASSPATH中,讓systemclassloader能夠識別和載入。然後我們通過自己的classloader來從指定的class檔案中載入這個Class(不能夠委託parent載入,因為這樣會被systemclassloader從CLASSPATH中將其載入),然後例項化一個Object,並造型成這個Class,這樣JVM也識別這個Class(因為systemclassloader能夠定位和載入這個Class從CLASSPATH中),載入的也不是CLASSPATH中的這個Class,而是從CLASSPATH外動態載入的,這樣總行了吧!十分不幸的是,這時會出現“java.lang.ClassCastException”違例。

為什麼呢?我們也來分析一下,不錯,我們雖然從CLASSPATH外使用我們自己的classloader動態載入了這個Class,但將它的例項造型的時候是JVM會使用systemclassloader來再次載入這個Class,並嘗試將使用我們的自己的classloader載入的Class的一個例項造型為systemclassloader載入的這個Class(另外的一個)。大家發現什麼問題了嗎?也就是我們嘗試將從一個classloader載入的Class的一個例項造型為另外一個classloader載入的Class,雖然這兩個Class的名字一樣,甚至是從同一個class檔案中載入。但不幸的是JVM卻認為這個兩個Class是不同的,即JVM認為不同的classloader載入的相同的名字的Class(即使是從同一個class檔案中載入的)是不同的!這樣做的原因我想大概也是主要出於安全性考慮,這樣就保證所有的核心Java類都是systemclassloader載入的,我們無法用自己的classloader載入的相同名字的Class的例項來替換它們的例項。

看到這裡,聰明的讀者一定想到了該如何動態載入我們的Class,例項化,造型並呼叫了吧!

那就是利用面向物件的基本特性之一的多形性。我們把我們動態載入的Class的例項造型成它的一個systemclassloader所能識別的父類就行了!這是為什麼呢?我們還是要再來分析一次。當我們用我們自己的classloader來動態載入這我們只要把這個Class的時候,發現它有一個父類Class,在載入它之前JVM先會載入這個父類Class,這個父類Class是systemclassloader所能識別的,根據委託機制,它將由systemclassloader載入,然後我們的classloader再載入這個Class,建立一個例項,造型為這個父類Class,注意了,造型成這個父類Class的時候(也就是上溯)是面向物件的java語言所允許的並且JVM也支援的,JVM就使用systemclassloader再次載入這個父類Class,然後將此例項造型為這個父類Class。大家可以從這個過程發現這個父類Class都是由system classloader載入的,也就是同一個classloader載入的同一個Class,所以造型的時候不會出現任何異常。而根據多形性,呼叫這個父類的方法時,真正執行的是這個Class(非父類Class)的覆蓋了父類方法的方法。這些方法中也可以引用systemclassloader不能識別的Class,因為根據全盤負責原則,只要載入這個Class的classloader即我們自己定義的classloader能夠定位和載入這些Class就行了。

這樣我們就可以事先定義好一組介面或者基類並放入CLASSPATH中,然後在執行的時候動態的載入實現或者繼承了這些介面或基類的子類。還不明白嗎?讓我們來想一想Servlet吧,web applicationserver能夠載入任何繼承了Servlet的Class並正確的執行它們,不管它實際的Class是什麼,就是都把它們例項化成為一個ServletClass,然後執行Servlet的init,doPost,doGet和destroy等方法的,而不管這個Servlet是從web-inf/lib和web-inf/classes下由systemclassloader的子classloader(即定製的classloader)動態載入。說了這麼多希望大家都明白了。在applet,ejb等容器中,都是採用了這種機制.

對於以上各種情況,希望大家實際編寫一些example來實驗一下。

最後我再說點別的,classloader雖然稱為類載入器,但並不意味著只能用來載入Class,我們還可以利用它也獲得圖片,音訊檔案等資源的URL,當然,這些資源必須在CLASSPATH中的jar類庫中或目錄下。我們來看API的doc中關於ClassLoader的兩個尋找資源和Class的方法描述吧:
        public URL getResource(String name)
        用指定的名字來查詢資源,一個資源是一些能夠被class程式碼訪問的在某種程度上依賴於程式碼位置的資料(圖片,音訊,文字等等)。
               一個資源的名字是以'/'號分隔確定資源的路徑名的。
              這個方法將先請求parentclassloader搜尋資源,如果沒有parent,則會在內建在虛擬機器中的classloader(即bootstrapclassloader)的路徑中搜索。如果失敗,這個方法將呼叫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:檔案:/C:/j2sdk1.4.1_01/jre/lib/rt.jar!/java/lang/String.class
表明String.class檔案在rt.jar的java/lang目錄中。
因此我們可以將圖片等資源隨同Class一同打包到jar類庫中(當然,也可單獨打包這些資源)並新增它們到class loader的搜尋路徑中,我們就可以無需關心這些資源的具體位置,讓class loader來幫我們尋找了!

原文引自:http://www.blogjava.net/lhulcn618/archive/2006/05/25/48230.html

當JVM(Java虛擬機器)啟動時,會形成由三個類載入器組成的初始類載入器層次結構:


       bootstrap classloader
                |
       extension classloader
                |
       system classloader

bootstrapclassloader-引導(也稱為原始)類載入器,它負責載入Java的核心類。在Sun的JVM中,在執行java的命令中使用-Xbootclasspath選項或使用- D選項指定sun.boot.class.path系統屬性值可以指定附加的類。這個載入器的是非常特殊的,它實際上不是java.lang.ClassLoader的子類,而是由JVM自身實現的。大家可以通過執行以下程式碼來獲得bootstrapclassloader載入了那些核心類庫:
   URL[] urls=sun.misc.Launcher.getBootstrapClassPath().getURLs();
   for (int i = 0; i < urls.length; i++) {
     System.out.println(urls.toExternalform());
   }
在我的計算機上的結果為:
檔案:/C:/j2sdk1.4.1_01/jre/lib/endorsed/dom.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/endorsed/sax.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xalan-2.3.1.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xercesImpl-2.0.0.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xml-apis.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xsltc.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/rt.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/i18n.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/sunrsasign.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/jsse.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/jce.jar
檔案:/C:/j2sdk1.4.1_01/jre/lib/charsets.jar
檔案:/C:/j2sdk1.4.1_01/jre/classes
這時大家知道了為什麼我們不需要在系統屬性CLASSPATH中指定這些類庫了吧,因為JVM在啟動的時候就自動載入它們了。

extensionclassloader-擴充套件類載入器,它負責載入JRE的擴充套件目錄(JAVA_HOME/jre/lib/ext或者由java.ext.dirs系統屬性指定的)中JAR的類包。這為引入除Java核心類以外的新功能提供了一個標準機制。因為預設的擴充套件目錄對所有從同一個JRE中啟動的JVM都是通用的,所以放入這個目錄的JAR類包對所有的JVM和systemclassloader都是可見的。在這個例項上呼叫方法getParent()總是返回空值null,因為引導載入器bootstrapclassloader不是一個真正的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:/j2sdk1.4.1_01/jre/lib/ext
the parent of extension classloader : null
extensionclassloader是system classloader的parent,而bootstrap classloader是extensionclassloader的parent,但它不是一個實際的classloader,所以為null。

systemclassloader-系統(也稱為應用)類載入器,它負責在JVM被啟動時,載入來自在命令java中的-classpath或者java.class.path系統屬性或者CLASSPATH作業系統屬性所指定的JAR類包和類路徑。總能通過靜態方法ClassLoader.getSystemClassLoader()找到該類載入器。如果沒有特別指定,則使用者自定義的任何類載入器都將該類載入器作為它的父載入器。執行以下程式碼即可獲得:
   System.out.println(System.getProperty("java.class.path"));
輸出結果則為使用者在系統屬性裡面設定的CLASSPATH。
classloader載入類用的是全盤負責委託機制。所謂全盤負責,即是當一個classloader載入一個Class的時候,這個Class所依賴的和引用的所有Class也由這個classloader負責載入,除非是顯式的使用另外一個classloader載入;委託機制則是先讓parent(父)類載入器(而不是super,它與parentclassloader類不是繼承關係)尋找,只有在parent找不到的時候才從自己的類路徑中去尋找。此外類載入還採用了cache機制,也就是如果cache中儲存了這個Class就直接返回它,如果沒有才從檔案中讀取和轉換成Class,並存入cache,這就是為什麼我們修改了Class但是必須重新啟動JVM才能生效的原因。


每個ClassLoader載入Class的過程是:
1.檢測此Class是否載入過(即在cache中是否有此Class),如果有到8,如果沒有到2
2.如果parent classloader不存在(沒有parent,那parent一定是bootstrap classloader了),到4
3.請求parent classloader載入,如果成功到8,不成功到5
4.請求jvm從bootstrap classloader中載入,如果成功到8
5.尋找Class檔案(從與此classloader相關的類路徑中尋找)。如果找不到則到7.
6.從檔案中載入Class,到8.
7.丟擲ClassNotFoundException.
8.返回Class.

其中5.6步我們可以通過覆蓋ClassLoader的findClass方法來實現自己的載入策略。甚至覆蓋loadClass方法來實現自己的載入過程。

類載入器的順序是:
先是bootstrap classloader,然後是extension classloader,最後才是systemclassloader。大家會發現載入的Class越是重要的越在靠前面。這樣做的原因是出於安全性的考慮,試想如果systemclassloader“親自”載入了一個具有破壞性的“java.lang.System”類的後果吧。這種委託機制保證了使用者即使具有一個這樣的類,也把它加入到了類路徑中,但是它永遠不會被載入,因為這個類總是由bootstrap classloader來載入的。大家可以執行一下以下的程式碼:
   System.out.println(System.class.getClassLoader());
將會看到結果是null,這就表明java.lang.System是由bootstrap classloader載入的,因為bootstrap classloader不是一個真正的ClassLoader例項,而是由JVM實現的,正如前面已經說過的。

下面就讓我們來看看JVM是如何來為我們來建立類載入器的結構的:
sun.misc.Launcher,顧名思義,當你執行java命令的時候,JVM會先使用bootstrap classloader載入並初始化一個Launcher,執行下來程式碼:
  System.out.println("the Launcher's classloader is "+sun.misc.Launcher.getLauncher().getClass().getClassLoader());
結果為:
  the Launcher's classloader is null (因為是用bootstrap classloader載入,所以class loader為null)
Launcher會根據系統和命令設定初始化好class loader結構,JVM就用它來獲得extension classloader和systemclassloader,並載入所有的需要載入的Class,最後執行java命令指定的帶有靜態的main方法的Class。extensionclassloader實際上是sun.misc.Launcher$ExtClassLoader類的一個例項,systemclassloader實際上是sun.misc.Launcher$AppClassLoader類的一個例項。並且都是java.net.URLClassLoader的子類。

讓我們來看看Launcher初試化的過程的部分程式碼。

Launcher的部分程式碼:
public class Launcher  {
   public Launcher() {
       ExtClassLoader extclassloader;
       try {
           //初始化extension classloader
           extclassloader = ExtClassLoader.getExtClassLoader();
       } catch(IOException ioexception) {
           throw new InternalError("Could not create extension class loader");
       }
       try {
           //初始化system classloader,parent是extension classloader
           loader = AppClassLoader.getAppClassLoader(extclassloader);
       } catch(IOException ioexception1) {
           throw new InternalError("Could not create application class loader");
       }
       //將system classloader設定成當前執行緒的context classloader(將在後面加以介紹)
       Thread.currentThread().setContextClassLoader(loader);
       ......
   }
   public ClassLoader getClassLoader() {
       //返回system classloader
       return loader;
   }
}

extension classloader的部分程式碼:
static class Launcher$ExtClassLoader extends URLClassLoader {

   public static Launcher$ExtClassLoader getExtClassLoader()
       throws IOException
   {
       File afile[] = getExtDirs();
       return (Launcher$ExtClassLoader)AccessController.doPrivileged(new Launcher$1(afile));
   }
  private static File[] getExtDirs() {
       //獲得系統屬性“java.ext.dirs”
       String s = System.getProperty("java.ext.dirs");
       File afile[];
       if(s != null) {
           StringTokenizer stringtokenizer = new StringTokenizer(s, File.pathSeparator);
           int i = stringtokenizer.countTokens();
           afile = new File;
           for(int j = 0; j < i; j++)
               afile[j] = new File(stringtokenizer.nextToken());

       } else {
           afile = new File[0];
       }
       return afile;
   }
}

system classloader的部分程式碼:
static class Launcher$AppClassLoader extends URLClassLoader
{

   public static ClassLoader getAppClassLoader(ClassLoader classloader)
       throws IOException
   {
       //獲得系統屬性“java.class.path”
       String s = System.getProperty("java.class.path");
       File afile[] = s != null ? Launcher.access$200(s) : new File[0];
       return (Launcher$AppClassLoader)AccessController.doPrivileged(new Launcher$2(s, afile, classloader));
   }
}

看了原始碼大家就清楚了吧,extensionclassloader是使用系統屬性“java.ext.dirs”設定類搜尋路徑的,並且沒有parent。systemclassloader是使用系統屬性“java.class.path”設定類搜尋路徑的,並且有一個parentclassloader。Launcher初始化extension classloader,systemclassloader,並將system classloader設定成為context classloader,但是僅僅返回systemclassloader給JVM。

  這裡怎麼又出來一個contextclassloader呢?它有什麼用呢?我們在建立一個執行緒Thread的時候,可以為這個執行緒通過setContextClassLoader方法來指定一個合適的classloader作為這個執行緒的contextclassloader,當此執行緒執行的時候,我們可以通過getContextClassLoader方法來獲得此contextclassloader,就可以用它來載入我們所需要的Class。預設的是systemclassloader。利用這個特性,我們可以“打破”classloader委託機制了,父classloader可以獲得當前執行緒的contextclassloader,而這個contextclassloader可以是它的子classloader或者其他的classloader,那麼父classloader就可以從其獲得所需的Class,這就打破了只能向父classloader請求的限制了。這個機制可以滿足當我們的classpath是在執行時才確定,並由定製的classloader載入的時候,由system classloader(即在jvmclasspath中)載入的class可以通過contextclassloader獲得定製的classloader並載入入特定的class(通常是抽象類和介面,定製的classloader中是其實現),例如web應用中的servlet就是用這種機制載入的.


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

這是j2se1.4 API的doc中URLClassLoader的兩個構造器的描述:
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.
其中URL[] urls就是我們要設定的類搜尋路徑,parent就是這個classloader的parent classloader,預設的是system classloader。


好,現在我們能夠動態的載入Class了,這樣我們就可以利用newInstance方法來獲得一個Object。但我們如何將此Object造型呢?可以將此Object造型成它本身的Class嗎?

首先讓我們來分析一下java原始檔的編譯,執行吧!javac命令是呼叫“JAVA_HOME/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:/ c:/Some.java
則字串陣列as為{"-classpath","c://foo//bar.jar;.","-d","c://","c://Some.java"},如果帶有PrintWriter引數,則會把編譯資訊出到這個指定的printWriter中。預設的輸出是System.err。

其中Main是由JVM使用Launcher初始化的systemclassloader載入的,根據全盤負責原則,編譯器在解析這個java原始檔時所發現的它所依賴和引用的所有Class也將由systemclassloader載入,如果system classloader不能載入某個Class時,編譯器將丟擲一個“cannot resolvesymbol”錯誤。

所以首先編譯就通不過,也就是編譯器無法編譯一個引用了不在CLASSPATH中的未知Class的java原始檔,而由於拼寫錯誤或者沒有把所需類庫放到CLASSPATH中,大家一定經常看到這個“cannot resolve symbol”這個編譯錯誤吧!

其次,就是我們把這個Class放到編譯路徑中,成功的進行了編譯,然後在執行的時候不把它放入到CLASSPATH中而利用我們自己的classloader來動態載入這個Class,這時候也會出現“java.lang.NoClassDefFoundError”的違例,為什麼呢?

我們再來分析一下,首先呼叫這個造型語句的可執行的Class一定是由JVM使用Launcher初始化的systemclassloader載入的,根據全盤負責原則,當我們進行造型的時候,JVM也會使用systemclassloader來嘗試載入這個Class來對例項進行造型,自然在systemclassloader尋找不到這個Class時就會丟擲“java.lang.NoClassDefFoundError”的違例。

OK,現在讓我們來總結一下,java檔案的編譯和Class的載入執行,都是使用Launcher初始化的systemclassloader作為類載入器的,我們無法動態的改變systemclassloader,更無法讓JVM使用我們自己的classloader來替換systemclassloader,根據全盤負責原則,就限制了編譯和執行時,我們無法直接顯式的使用一個systemclassloader尋找不到的Class,即我們只能使用Java核心類庫,擴充套件類庫和CLASSPATH中的類庫中的Class。

還不死心!再嘗試一下這種情況,我們把這個Class也放入到CLASSPATH中,讓systemclassloader能夠識別和載入。然後我們通過自己的classloader來從指定的class檔案中載入這個Class(不能夠委託parent載入,因為這樣會被systemclassloader從CLASSPATH中將其載入),然後例項化一個Object,並造型成這個Class,這樣JVM也識別這個Class(因為systemclassloader能夠定位和載入這個Class從CLASSPATH中),載入的也不是CLASSPATH中的這個Class,而是從CLASSPATH外動態載入的,這樣總行了吧!十分不幸的是,這時會出現“java.lang.ClassCastException”違例。

為什麼呢?我們也來分析一下,不錯,我們雖然從CLASSPATH外使用我們自己的classloader動態載入了這個Class,但將它的例項造型的時候是JVM會使用systemclassloader來再次載入這個Class,並嘗試將使用我們的自己的classloader載入的Class的一個例項造型為systemclassloader載入的這個Class(另外的一個)。大家發現什麼問題了嗎?也就是我們嘗試將從一個classloader載入的Class的一個例項造型為另外一個classloader載入的Class,雖然這兩個Class的名字一樣,甚至是從同一個class檔案中載入。但不幸的是JVM卻認為這個兩個Class是不同的,即JVM認為不同的classloader載入的相同的名字的Class(即使是從同一個class檔案中載入的)是不同的!這樣做的原因我想大概也是主要出於安全性考慮,這樣就保證所有的核心Java類都是systemclassloader載入的,我們無法用自己的classloader載入的相同名字的Class的例項來替換它們的例項。

看到這裡,聰明的讀者一定想到了該如何動態載入我們的Class,例項化,造型並呼叫了吧!

那就是利用面向物件的基本特性之一的多形性。我們把我們動態載入的Class的例項造型成它的一個systemclassloader所能識別的父類就行了!這是為什麼呢?我們還是要再來分析一次。當我們用我們自己的classloader來動態載入這我們只要把這個Class的時候,發現它有一個父類Class,在載入它之前JVM先會載入這個父類Class,這個父類Class是systemclassloader所能識別的,根據委託機制,它將由systemclassloader載入,然後我們的classloader再載入這個Class,建立一個例項,造型為這個父類Class,注意了,造型成這個父類Class的時候(也就是上溯)是面向物件的java語言所允許的並且JVM也支援的,JVM就使用systemclassloader再次載入這個父類Class,然後將此例項造型為這個父類Class。大家可以從這個過程發現這個父類Class都是由system classloader載入的,也就是同一個classloader載入的同一個Class,所以造型的時候不會出現任何異常。而根據多形性,呼叫這個父類的方法時,真正執行的是這個Class(非父類Class)的覆蓋了父類方法的方法。這些方法中也可以引用systemclassloader不能識別的Class,因為根據全盤負責原則,只要載入這個Class的classloader即我們自己定義的classloader能夠定位和載入這些Class就行了。

這樣我們就可以事先定義好一組介面或者基類並放入CLASSPATH中,然後在執行的時候動態的載入實現或者繼承了這些介面或基類的子類。還不明白嗎?讓我們來想一想Servlet吧,web applicationserver能夠載入任何繼承了Servlet的Class並正確的執行它們,不管它實際的Class是什麼,就是都把它們例項化成為一個ServletClass,然後執行Servlet的init,doPost,doGet和destroy等方法的,而不管這個Servlet是從web-inf/lib和web-inf/classes下由systemclassloader的子classloader(即定製的classloader)動態載入。說了這麼多希望大家都明白了。在applet,ejb等容器中,都是採用了這種機制.

對於以上各種情況,希望大家實際編寫一些example來實驗一下。

最後我再說點別的,classloader雖然稱為類載入器,但並不意味著只能用來載入Class,我們還可以利用它也獲得圖片,音訊檔案等資源的URL,當然,這些資源必須在CLASSPATH中的jar類庫中或目錄下。我們來看API的doc中關於ClassLoader的兩個尋找資源和Class的方法描述吧:
        public URL getResource(String name)
        用指定的名字來查詢資源,一個資源是一些能夠被class程式碼訪問的在某種程度上依賴於程式碼位置的資料(圖片,音訊,文字等等)。
               一個資源的名字是以'/'號分隔確定資源的路徑名的。
              這個方法將先請求parentclassloader搜尋資源,如果沒有parent,則會在內建在虛擬機器中的classloader(即bootstrapclassloader)的路徑中搜索。如果失敗,這個方法將呼叫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:檔案:/C:/j2sdk1.4.1_01/jre/lib/rt.jar!/java/lang/String.class
表明String.class檔案在rt.jar的java/lang目錄中。
因此我們可以將圖片等資源隨同Class一同打包到jar類庫中(當然,也可單獨打包這些資源)並新增它們到class loader的搜尋路徑中,我們就可以無需關心這些資源的具體位置,讓class loader來幫我們尋找了!