閒聊ClassLoader的父加載器

Java使用類加載器來裝載字節碼到內存,以便後續用來創建對象調用方法等。就目前的JVM,要說這個ClassLoader,先要說到它的委託模型(有人將parent譯作雙親,雙親委派模型,竊以爲,很不準確,原因在說完這個委託模型之後講)。何爲委託模型?java.lang.ClassLoader有這樣的描述:

每個 ClassLoader 實例都有一個相關的父類加載器。
需要查找類或資源時,ClassLoader 實例會在試圖親自查找類或資源之前,將搜索類或資源的任務委託給其父類加載器。
虛擬機的內置類加載器(稱爲 "bootstrap class loader")本身沒有父類加載器,但是可以將它用作 ClassLoader 實例的父類加載器。 


開始說“每個 ClassLoader 實例都有一個相關的父類加載器”,後面又說“虛擬機的內置類加載器(稱爲 “bootstrap class loader”)本身沒有父類加載器”,這不是自行矛盾嗎!其實不然,前面說的“每個 ClassLoader 實例”指的是每個java.lang.ClassLoader(該類是抽象類)子類的對象。而bootstrap class loader(後面叫它引導類加載器)是jvm內部由c++實現的,並不繼承java.lang.ClassLoader類,所以它不屬於“ClassLoader 實例”,也沒有辦法在Java代碼中獲取到它。

從API描述中還可以得到的信息是,1、引導類裝載器沒有父類裝載器,雖然不是ClassLoader的實例,但是可以作爲其它ClassLoader實例的父類加載器;2、 每個java.lang.ClassLoader子類的對象都關聯着一個父類加載器。也就是說這委託模型一種樹狀的模型,一個ClassLoader子類有且只有一個父類加載器,多個ClassLoader子類的父類加載器可以是同一個。這也是爲什麼我前面說“雙親委派模型”這種說法很不準確的原因。

類加載器可以分爲兩類:一是引導類裝載器(c++實現,非ClassLoader的實例,用於加載java類庫中的類);二是自定義類裝載器(即所有繼承了java.lang.ClassLoader的類加載器,它們本身是由引導類裝載器裝載進jvm的).在現在的JRE中,已經自帶了一些自定義類加載器,常見且有名的有:擴展類裝載器(其父類加載器爲引導類加載器,用於加載jre的lib/ext目錄中的類),系統類加載器(其父類加載器爲擴展類加載器,用於加載classpath中的類)。可以通過一段小代碼來了解:

ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("系統類裝載器:" + systemClassLoader);
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println("系統類裝載器的父類加載器——擴展類加載器:" + extClassLoader);
ClassLoader bootClassLoader = extClassLoader.getParent();
System.out.println("擴展類加載器的父類加載器——引導類加載器:" + bootClassLoader);

運行結果如下:

系統類裝載器:sun.misc.Launcher$AppClassLoader@1b000e7
系統類裝載器的父類加載器——擴展類加載器:sun.misc.Launcher$ExtClassLoader@b76fa
擴展類加載器的父類加載器——引導類加載器:null

觀察ClassLoader類的構造方法可以發現,可以顯式指定父類加載器,也可以使用默認的形式。有些文章或資料上寫到“默認的父類加載器爲系統類加載器”,“默認的父類加載器爲引導類加載器”,這裏我又覺得含糊不清了,爲什麼?默認,這裏可以有兩種理解:一、使用ClassLoader() 不帶參數的構造方法時其父類加載器是什麼。二、使用ClassLoader(ClassLoader parent)這種傳入null作爲參數的構造方法時其父類加載器是什麼。這兩種“默認情況”是不一樣的。當使用ClassLoader(ClassLoader parent)傳入null的時候,其父類加載器是引導類加載器(當然,也可以將null理解成引導類加載器);當使用沒有參數的ClassLoader()時,其父類加載器一般爲系統類裝載器,這個構造方法等價於ClassLoader(ClassLoader.getSystemClassLoader()),前面說這種方式“其父類加載器一般爲系統類裝載器”,是因爲getSystemClassLoader方法是有可能返回null的,具體參見getSystemClassLoader的API文檔。

這個委託模型是如何工作的呢?假設有以下模型,A→B→System(系統類加載器)→Ext(擴展類加載器)→Boot(引導類加載器),箭頭那邊是非箭頭邊的父類加載器,B是A的父類加載器,系統類加載器是B的父類加載器,以此類推。當讓加載器A去加載一個類(假設這個類是C,位於在classpath中)的時候,A並不先自己去加載這個類,而是委託給其父類加載器,父類加載器執行同樣的動作,直到沒有父類加載器爲止(一般是到了引導類加載器),如果該模型的頂端那個類加載器沒辦法加載指定的類,就會回退到Ext,Ext發現其也不能裝載classpath中的類,就繼續回退到System,這時,System發現其能裝載指定的類了。在這個過程中,真正裝載類的那個類裝載器稱作定義類裝載器,導致類被加載的加載器稱爲初始類裝載器,這個例子中,A,B,System都是初始類裝載器,System是定義類裝載器。定義類裝載器是一個特殊的初始類裝載器,而Ext,Boot既不是定義類裝載器也不是初始類裝載器。

每個加載器都有一個命名空間,所謂的命名空間就是加載器維護了一張表,表的內容爲其作爲初始類裝載器所加載的類,也就是說,如加載器L,不管類是不是由L裝載進jvm的,只要是L的父類加載器路徑中的某個加載器裝載的,L都會將這個裝載的類記錄下來,下次再使用L加載同樣的類時,就會返回這個已經裝載過的類。在上面的例子中,A,B,System的命名空間中都包含類C,下次B或者System需要裝載C的時候,就會直接返回這個已經裝載的類。

這樣一來,不難發現,類的全限定名並不能唯一確定jvm中裝載的類,還要加上裝載該類的定義類裝載器,在java.lang.Class中有方法getClassLoader,它返回的就是裝載該類的定義類加載器,如果是引導類加載器,返回的是null。那麼,不同的裝載器裝載同一個類後,他們的對象能夠賦值給對方的引用嗎?來看個例子(如無特別說明,這裏的例子都只能運行於eclipse中,且eclipse中存放編譯後的class文件的目錄叫做bin,要運行於其它環境,需要修改程序):
先來一個Person類:

package com.ticmy.classloader;
public class Person {}

再看測試代碼:

package com.ticmy.classloader;
import java.net.URL;
import java.net.URLClassLoader;
public class TestClassLoader {
    public static void main(String[] args) throws Exception {
        String url = "file://" + System.getProperty("user.dir").replaceAll("\\\\", "/")
                + "/bin/";
        System.out.println(url);
        ClassLoader c1 = new URLClassLoader(new URL[]{new URL(url)}, null);
        System.out.println("c1的父類加載器: " + c1.getParent());
        System.out.println("SystemClassLoader: " + ClassLoader.getSystemClassLoader());
        Class<?> class1 = c1.loadClass("com.ticmy.classloader.Person");
        Object o  = class1.newInstance();
        System.out.println("Person:" + o);
        System.out.println("Test的定義類裝載器: " + TestClassLoader.class.getClassLoader());
        System.out.println("Test中直接使用Person使用的ClassLoader: " + Person.class.getClassLoader());
        System.out.println("自定義裝載器裝載Person的定義類加載器: " + o.getClass().getClassLoader());
         
        Person p = (Person)o;
    }
}

運行結果如下:

file://E:/workSpace/test/bin/
c1的父類加載器: null
SystemClassLoader: sun.misc.Launcher$AppClassLoader@1b000e7
Person:com.ticmy.classloader.Person@19e8f17
Test的定義類裝載器: sun.misc.Launcher$AppClassLoader@1b000e7
Test中直接使用Person使用的ClassLoader: sun.misc.Launcher$AppClassLoader@1b000e7
自定義裝載器裝載Person的定義類加載器: java.net.URLClassLoader@15093f1
Exception in thread "main" java.lang.ClassCastException: com.ticmy.classloader.Person cannot be cast to com.ticmy.classloader.Person
	at com.ticmy.classloader.TestClassLoader.main(TestClassLoader.java:21)

new URLClassLoader的時候,其父加載器傳的是null,也就是說其父類加載器是引導類加載器。url所指的路徑,既屬於系統類加載器尋找的classpath(所以可以直接在程序中new Person),又屬於c1查找類的路徑。程序制定c1去裝載com.ticmy.classloader.Person,首先會委派給其父加載器——引導類加載器——去裝載,引導類加載器發現自己找不到指定的類,於是回退到c1自身去裝載這個類,而c1能找到這個類,所以class1的定義類加載器就是c1。而在程序中直接寫Person p = …的這種形式,Person是存在於TestClassLoader常量池的符號引用中的,當需要用到Person的時候,會使用裝載TestClassLoader類的裝載器去裝載,所以,直接寫Person p = …其裝載器爲系統類裝載器,也就是hotspot中的sun.misc.Launcher$AppClassLoader。這樣,創建一個由class1裝載的Person的對象轉換成由系統類裝載器裝載的Person的引用,就報ClassCastException,無法轉換,因爲它們已經不屬於同一個類了。

若是將com.ticmy.classloader.Person換成java.lang.String呢?

package com.ticmy.classloader;
import java.net.URL;
import java.net.URLClassLoader;
public class TestClassLoader {
    public static void main(String[] args) throws Exception {
        String url = "file://" + System.getProperty("user.dir").replaceAll("\\\\", "/")
                + "/bin/";
        System.out.println(url);
        ClassLoader c1 = new URLClassLoader(new URL[]{new URL(url)}, null);
        System.out.println("c1的父類加載器: " + c1.getParent());
        System.out.println("SystemClassLoader: " + ClassLoader.getSystemClassLoader());
        Class<?> class1 = c1.loadClass("java.lang.String");
        Object o  = class1.newInstance();
        System.out.println("Person:" + o);
        System.out.println("Test的定義類裝載器: " + TestClassLoader.class.getClassLoader());
        System.out.println("Test中直接使用Person使用的ClassLoader: " + Person.class.getClassLoader());
        System.out.println("自定義裝載器裝載Person的定義類加載器: " + o.getClass().getClassLoader());
         
        String p = (String)o;
    }
}

運行之後發現沒有報錯。這是因爲c1去裝載java.lang.String的時候委託給引導類加載器裝載,引導類加載器是可以加載的,其生成的class1的類是java.lang.String,定義類加載器是引導類加載器。而直接寫的形式中,由系統類加載器發起裝載請求,系統類加載器將其委託給擴展類加載器,擴展類加載器再委託給引導類加載器,最終引導類加載器可以加載java.lang.String(已經加載過就直接返回加載過的)。這樣直接寫的String的類名是java.lang.String,定義類加載器也是引導類加載器,所以由c1發起裝載的java.lang.String的對象是可以轉換成由系統類加載器發起裝載的java.lang.String的引用的。

前面new URLClassLoader傳的是null參數,如果使用無參構造呢?

package com.ticmy.classloader;
import java.net.URL;
import java.net.URLClassLoader;
public class TestClassLoader {
    public static void main(String[] args) throws Exception {
        String url = "file://" + System.getProperty("user.dir").replaceAll("\\\\", "/")
                + "/bin/";
        System.out.println(url);
        ClassLoader c1 = new URLClassLoader(new URL[]{new URL(url)});
        System.out.println("c1的父類加載器: " + c1.getParent());
        System.out.println("SystemClassLoader: " + ClassLoader.getSystemClassLoader());
        Class<?> class1 = c1.loadClass("com.ticmy.classloader.Person");
        Object o  = class1.newInstance();
        System.out.println("Person:" + o);
        System.out.println("Test的定義類裝載器: " + TestClassLoader.class.getClassLoader());
        System.out.println("Test中直接使用Person使用的ClassLoader: " + Person.class.getClassLoader());
        System.out.println("自定義裝載器裝載Person的定義類加載器: " + o.getClass().getClassLoader());
         
        Person p = (Person)o;
    }
}

運行之後也是沒有問題的。前面已經說到兩個默認,這種“默認”的父類加載器就是系統類加載器,而系統類加載器是可以加載Person的,因此,兩個不同的類加載器發起的對Person的裝載,最後,他們都是由同一個類加載器裝載的,也就說,兩種情況下,Person的定義類加載器都是系統類加載器,故,他們可以轉換。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章