類載入器 - 名稱空間
本部落格將沿用上篇部落格中展示的自定義類載入器程式碼
複雜類載入情況分析
測試程式碼一
首先,新建一個類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類空間。