1. 程式人生 > >類載入器 - 名稱空間

類載入器 - 名稱空間

本部落格將沿用上篇部落格中展示的自定義類載入器程式碼

複雜類載入情況分析

測試程式碼一

首先,新建一個類Test14,重寫預設的構造方法,列印載入該類的類載入器

public class Test14 {
    public Test14() {
        System.out.println("Test14 is loaded by:" + this.getClass().getClassLoader());
    }
}

然後,在新建一個類Test15,同樣重寫預設的構造方法,列印載入該類的類載入器,在構造方法中new出Test14的例項

public class Test15 {
    public Test15() {
        System.out.println("Test15 is loaded by:" + this.getClass().getClassLoader());

        new Test14();
    }
}

測試程式碼

public class Test16 {
    public static void main(String[] args) throws Exception {
        test01();
    }

    private static void test01 () throws Exception {
        ClassLoaderTest classLoader = new ClassLoaderTest("classLoader");
        Class<?> clazz = classLoader.loadClass("classloader.Test15");
        System.out.println("class:" + clazz);
        Object object = clazz.newInstance();
    }
}

猜測一下,首先自定義類載入器classLoader通過反射獲取Test15的Class物件,屬於主動使用,會載入Test15,classLoader委託它的父載入器AppClassLoader載入Test15;然後我們通過clazz.newInstance();程式碼獲取Test15的例項,呼叫Test15的構造方法,在Test15的構造方法中建立了Test14的例項,所以同樣載入了Test14,並呼叫了Test14的構造方法。加上-XX:+TraceClassLoading指令執行程式碼,發現執行結果和我們想的是一樣的。

......
[Loaded classloader.Test15 from file:/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/]
class:class classloader.Test15
Test15 is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
[Loaded classloader.Test14 from file:/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/]
Test14 is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
......

測試程式碼二

在上篇部落格中,自定義類載入器ClassLoaderTest是有一個path屬性可以自定義類的載入路徑的,我們同樣測試一下,我們將Test14和Test15的class檔案放到桌面的classloader資料夾下,然後刪除工程路徑下的class檔案,執行一下的測試程式碼

public class Test16 {
    public static void main(String[] args) throws Exception {
        test02();
    }
    private static void test02 () throws Exception {
        ClassLoaderTest classLoader = new ClassLoaderTest("classLoader");
        classLoader.setPath("/home/fanxuan/桌面/");
        Class<?> clazz = classLoader.loadClass("classloader.Test15");
        System.out.println("class:" + clazz);
        Object object = clazz.newInstance();
    }
}

按照上節的結果,應該都是ClassLoaderTest載入器載入了Test14和Test15類

class:class classloader.Test15
Test15 is loaded by:classloader.ClassLoaderTest@6d6f6e28
Test14 is loaded by:classloader.ClassLoaderTest@6d6f6e28

接下來,我們重新編譯專案,刪除掉工程目錄下的Test14的calss檔案,再次執行程式碼

class:class classloader.Test15
Test15 is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
Exception in thread "main" java.lang.NoClassDefFoundError: classloader/Test14
    at classloader.Test15.<init>(Test15.java:11)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at java.lang.Class.newInstance(Class.java:442)
    at classloader.Test16.test02(Test16.java:25)
    at classloader.Test16.main(Test16.java:9)
Caused by: java.lang.ClassNotFoundException: classloader.Test14
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 8 more

我們發現結果報錯了,按照我們正常的思維,自定義記載器classLoader委託父載入器AppClassLoader載入Test15,從列印結果可以看出Test15載入成功了,然後建立Test15的例項,載入Test14,因為工程目錄下缺少Test14的class檔案,所以AppClassLoader無法載入到Test14,由自定義載入器classLoader自身從桌面載入Test14。但是我們發現載入Test14的報了ClassNotFoundException的錯誤,這是因為在Test15中記載Test14的時候,是以Test15的類載入器AppClassLoader來載入的,AppClassLoader載入不到Test14,它的父載入器擴充套件類載入器同樣載入不到,擴充套件類載入器的父載入器啟動類載入器也載入不到,所以報錯ClassNotFoundException

然後,再重新編譯專案,刪除掉工程目錄下的Test15的calss檔案,再次執行程式碼。根據前文分析的程式碼,我們可以很清晰的得出結論:由自定義記載器classLoader載入了Test15,由系統類記載器AppClassLoader載入了Test14。

class:class classloader.Test15
Test15 is loaded by:classloader.ClassLoaderTest@6d6f6e28
Test14 is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2

測試程式碼三

簡單修改下Test14類,在Test14的構造方法中引用Test15的Class物件。

public class Test14 {
    public Test14() {
        System.out.println("Test14 is loaded by:" + this.getClass().getClassLoader());

        System.out.println("Test14:" + Test15.class);
    }
}

執行測試程式碼二中的測試程式碼Test16,結果如下,沒有任何問題。

class:class classloader.Test15
Test15 is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
Test14 is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
Test14:class classloader.Test15

我們同樣重新編譯專案,刪除掉工程目錄下的Test15的calss檔案,再次執行程式碼。

class:class classloader.Test15
Test15 is loaded by:classloader.ClassLoaderTest@6d6f6e28
Test14 is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
Exception in thread "main" java.lang.NoClassDefFoundError: classloader/Test15
    at classloader.Test14.<init>(Test14.java:11)
    at classloader.Test15.<init>(Test15.java:11)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at java.lang.Class.newInstance(Class.java:442)
    at classloader.Test16.test02(Test16.java:25)
    at classloader.Test16.main(Test16.java:9)
Caused by: java.lang.ClassNotFoundException: classloader.Test15
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 9 more

我們發現載入已經完成了,但是程式還是報錯了,是我們剛剛加的System.out.println("Test14:" + Test15.class);程式碼報的錯,依然是ClassNotFoundException錯誤。

分析:
Test15由自定義記載器classLoader載入,Test14由系統類記載器AppClassLoader載入。導致程式報錯的是因為名稱空間的問題,我們在上一篇部落格的結尾簡單介紹了名稱空間:每個類載入器都有自己的名稱空間,名稱空間由該載入器及所有的父載入器所載入的類組成。子載入器所載入的類可以看見父載入器載入的類,但是父載入器所載入的類無法看見子載入器載入的類。Test14是由AppClassLoader載入的,在AppClassLoader的名稱空間中沒有Test15的,所以程式報錯了。

名稱空間例項分析

測試程式碼

新建Entity類用於測試

public class Entity {
    private Entity entity;

    public void setEntity(Object entity) {
        this.entity = (Entity)entity;
    }
}

編寫測試程式碼

public class Test17 {
    public static void main(String[] args) throws Exception {
        ClassLoaderTest classLoader1 = new ClassLoaderTest("classLoader1");
        ClassLoaderTest classLoader2 = new ClassLoaderTest("classLoader2");

        Class<?> clazz1 = classLoader1.loadClass("classloader.Entity");
        Class<?> clazz2 = classLoader2.loadClass("classloader.Entity");

        System.out.println(clazz1 == clazz2);

        Object object1 = clazz1.newInstance();
        Object object2 = clazz2.newInstance();

        Method method = clazz1.getMethod("setEntity", Object.class);
        method.invoke(object1, object2);
    }
}

執行程式,System.out.println(clazz1 == clazz2);返回結果為true,都是AppClassLoader載入的,classLoader1載入之後會在AppClassLoader的名稱空間中形成快取,classLoader2載入的時候直接返回名稱空間已經存在的Class物件,所以clazz1與clazz2相同。

改造下程式碼,將Entity類的class檔案copy到桌面資料夾下,刪除工程下的class檔案,執行如下程式碼

public class Test18 {
    public static void main(String[] args) throws Exception {
        ClassLoaderTest classLoader1 = new ClassLoaderTest("classLoader1");
        ClassLoaderTest classLoader2 = new ClassLoaderTest("classLoader2");

        classLoader1.setPath("/home/fanxuan/桌面/");
        classLoader2.setPath("/home/fanxuan/桌面/");

        Class<?> clazz1 = classLoader1.loadClass("classloader.Entity");
        Class<?> clazz2 = classLoader2.loadClass("classloader.Entity");

        System.out.println(clazz1 == clazz2);

        Object object1 = clazz1.newInstance();
        Object object2 = clazz2.newInstance();

        Method method = clazz1.getMethod("setEntity", Object.class);
        method.invoke(object1, object2);
    }
}

根據前文的介紹,不難推斷System.out.println(clazz1 == clazz2);的執行結果為false,classLoader1和classLoader2分別載入了Entity類,就是其自身載入的(定義類載入器),在jvm的記憶體中形成了完全獨立的兩個名稱空間,所以clazz1與clazz2不同。而且因為clazz1和clazz2相互不可見,呼叫了classLoader1名稱空間中的方法,傳入了classLoader2名稱空間的物件,導致程式丟擲了異常。

false
Exception in thread "main" java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at classloader.Test18.main(Test18.java:26)
Caused by: java.lang.ClassCastException: classloader.Entity cannot be cast to classloader.Entity
    at classloader.Entity.setEntity(Entity.java:11)
    ... 5 more

不同類載入器的名稱空間關係

  • 同一名稱空間內的類是相互可見的
  • 子載入器的名稱空間包含所有父載入器的名稱空間,由子載入器所載入的類可以看見父載入器載入的類
  • 由父載入器所載入的類無法看見子載入器載入的類
  • 如果兩個載入器之間沒有任何直接或間接的父子關係,那麼它們各自載入的類相互不可見

父親委託機制的好處

在上篇部落格的2.1章節簡單介紹了一下類載入器的父親委託機制,這裡面來總結一下好處

  • 確保Java核心類庫的安全:所有的Java應用都至少會引用java.lang.Object類,也就是說在執行期,java.lang.Object類會被記載到Java虛擬機器當中;如果這個載入過程是由Java應用自己的類載入器所完成的,那麼可能會在JVM中存在多個版本的java.lang.Object類,而且這些類還是不相容的、相互不可見的(因為名稱空間的原因)。藉助父親委託機制,Java核心類庫中的類的載入工作都是由啟動類載入器來統一完成的,從而確保了Java應用所使用的都是同一個版本的Java核心類庫,他們之間是互相相容的。
  • 確保Java核心類庫提供的類不會被自定義的類所替代。
  • 不同的類載入器可以為相同名稱(binary name)的類建立額外的名稱空間。相同名稱的類可以並存在Java虛擬機器中,只需要用不同的類載入器來加他們即可,不同類載入器所載入的類是不相容的,這就相當於在Java虛擬機器內部建立了一個又一個相互隔離的Java類空間。