1. 程式人生 > 實用技巧 >【Java虛擬機器9】類載入器之名稱空間詳解

【Java虛擬機器9】類載入器之名稱空間詳解

前言

前面介紹類載入器的時候,介紹了一下名稱空間這個概念。今天就通過一個例子,來詳細瞭解一下【類載入器的名稱空間】。然後通過這個例子,我們可以總結一下雙親委託模型的好處與優點。

例1(不刪除classpath下的class檔案)

首先定義一個MyPerson

package com.jamie.jvmstudy;

public class MyPerson {
 
    private MyPerson myPerson;
 
    public void  setMyPerson(Object obj){
        this.myPerson = (MyPerson)obj;
    }
}

然後是自定義類載入器

package com.jamie.jvmstudy;

import java.io.*;

public class CustomizedClassLoader extends ClassLoader {

    private String classLoaderName;

    private String path;

    private String fileExtension = ".class";

    public CustomizedClassLoader(String classLoaderName) {
        super();
        this.classLoaderName = classLoaderName;
    }

    public CustomizedClassLoader(ClassLoader parent, String classLoaderName) {
        super(parent);
        this.classLoaderName = classLoaderName;
    }

    @Override
    public Class<?> findClass(String className) throws ClassNotFoundException {
        System.out.println("findClass invoked : " + className);
        System.out.println("class loader name : " + this.classLoaderName);
        byte[] data = this.loadClassData(className);

        return this.defineClass(className, data, 0, data.length);
    }

    private byte[] loadClassData(String className) {
        byte[] data = null;
        className = className.replace(".", "/");
        try(InputStream is = new FileInputStream(new File(this.path + className + this.fileExtension));
            ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            int ch;
            while(-1 != (ch = is.read())) {
                baos.write(ch);
            }
            data = baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }

    public void setPath(String path) {
        this.path = path;
    }
}

測試客戶端類

package com.jamie.jvmstudy;

import java.lang.reflect.Method;

public class TestClassLoaderNameSpace {
    public static void main(String[] args) throws Exception {
        CustomizedClassLoader loader1 = new CustomizedClassLoader("loader1");
        CustomizedClassLoader loader2 = new CustomizedClassLoader("loader2");
 
        Class<?> clazz1 = loader1.loadClass("com.jamie.jvmstudy.MyPerson");
        Class<?> clazz2 = loader2.loadClass("com.jamie.jvmstudy.MyPerson");

        System.out.println("clazz1的classLoader是" + clazz1.getClassLoader());
        System.out.println("clazz2的classLoader是" + clazz2.getClassLoader());
        System.out.println( clazz1 == clazz2);
 
        Object object1 = clazz1.newInstance();
        Object object2 = clazz2.newInstance();
        Method method = clazz1.getMethod("setMyPerson", Object.class);
        method.invoke(object1, object2);
    }
}

結果:

clazz1的classLoader是sun.misc.Launcher$AppClassLoader@14dad5dc
clazz2的classLoader是sun.misc.Launcher$AppClassLoader@14dad5dc
true

說明:

同一個類載入器(本例是應用類載入器)載入同一個類,得到的class物件是相同的。

例2(基於例1修改,刪除classpath下的class檔案)

操作

為自定義類載入器設定path,然後編譯成功後,刪除掉classpath下面的MyPerson.class檔案,把編譯出的MyPerson.class檔案移動到D:/temp資料夾裡面。

public class TestClassLoaderNameSpace {
    public static void main(String[] args) throws Exception {
        CustomizedClassLoader loader1 = new CustomizedClassLoader("loader1");
        CustomizedClassLoader loader2 = new CustomizedClassLoader("loader2");

        loader1.setPath("D:/temp/");
        loader2.setPath("D:/temp/");

        Class<?> clazz1 = loader1.loadClass("com.jamie.jvmstudy.MyPerson");
        Class<?> clazz2 = loader2.loadClass("com.jamie.jvmstudy.MyPerson");

        System.out.println("clazz1的classLoader是" + clazz1.getClassLoader());
        System.out.println("clazz2的classLoader是" + clazz2.getClassLoader());
        System.out.println( clazz1 == clazz2);
 
        Object object1 = clazz1.newInstance();
        Object object2 = clazz2.newInstance();
        Method method = clazz1.getMethod("setMyPerson", Object.class);
        method.invoke(object1, object2);
    }
}

程式碼執行結果

findClass invoked : com.jamie.jvmstudy.MyPerson
class loader name : loader1
findClass invoked : com.jamie.jvmstudy.MyPerson
class loader name : loader2
clazz1的classLoader是com.jamie.jvmstudy.CustomizedClassLoader@677327b6
clazz2的classLoader是com.jamie.jvmstudy.CustomizedClassLoader@7f31245a
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:497)
	at com.intellij.rt.execution.CommandLineWrapper.main(CommandLineWrapper.java:67)
Caused by: 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:497)
	at com.jamie.jvmstudy.TestClassLoaderNameSpace.main(TestClassLoaderNameSpace.java:23)
	... 5 more
Caused by: java.lang.ClassCastException: com.jamie.jvmstudy.MyPerson cannot be cast to com.jamie.jvmstudy.MyPerson
	at com.jamie.jvmstudy.MyPerson.setMyPerson(MyPerson.java:8)
	... 10 more

結論

loader1和loader2分別載入了MyPerson.class,分別給MyPerson.class分配了記憶體空間,如下圖:

這2個class物件雖然在檔案系統是來自於同一個class檔案,但是由於他們是被自定義類載入器載入的,並且這2個自定義類載入器是同級的,沒有父子關係。所以雙親委派模型中,他們是看不到對方的名稱空間的。
所以clazz1 == clazz2的結果為false

自己畫了一個簡圖:

回顧名稱空間概念

  • 每個類載入器都有自己的名稱空間。名稱空間由該載入器和所有父載入器所載入的類組成。(請結合下圖一起看,想明白)
  • 在同一個名稱空間中,不會出現類的完整名字(包括類的包名)相同的兩個類。
  • 在不同的名稱空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類。

雙親委託模型的好處

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