1. 程式人生 > 實用技巧 >Java虛擬機器之類載入器

Java虛擬機器之類載入器

類載入器介紹

類載入器負責將class檔案載入到記憶體中,併為之生成對應的java.lang.Class物件。對於任意一個類,都需要載入它的類載入器和這個類本身來確定該類在JVM中唯一性,也就是說,同一個class檔案用兩個不同的類載入器載入並建立兩個java.lang.Class物件,即使兩個物件來源自同一個class檔案,它們也是不相等的,這裡“相等”包括Class物件的equals()方法、isAssignableFrom()方法、isInstance()方法,也包括使用instanceof關鍵字做物件所屬關係判定情況。

類載入器分類

虛擬機器提供了3種類載入器,啟動(Bootstrap)類載入器、擴充套件(Extension)類載入器、應用程式(Application)類載入器(也稱應用類載入器)

Bootstrap ClassLoader

該類載入器沒有父類載入器,它負責載入虛擬機器的核心類庫。Bootstrap ClassLoader載入器用於在啟動JVM時載入類,以使JVM能正常工作,因而它是用Native(c++)程式碼實現的,最早被創建出來,處於最底層。它並沒有繼承java.lang.ClassLoader類。

我們可以來看下Bootstrap ClassLoader載入了哪些類,檢視Bootstrap ClassLoader載入的類可以通過System.getProperty("sun.boot.class.path")看到:

public class ClassLoaderTest {
    public static void main(String[] args) {
        String path = System.getProperty("sun.boot.class.path");
        Arrays.asList(path.split(";")).forEach(System.out::println);
    }
}

結果:

Extension ClassLoader

該類載入器的父類載入器是根類載入器。它從java.ext.dirs系統屬性所指定的目錄獲取載入類庫或從JDK的安裝目錄的jre\lib\ext子目錄下載入類庫。如果把jar放到這個目錄下,也會自動用擴充套件類載入器載入。擴充套件類載入器是java類,是java.lang.ClassLoader類的子類。

Application ClassLoader

應用類載入器它的父類載入器是擴充套件類載入器,它將載入CLASSPATH中配置的目錄和jar檔案,它是使用者自定義類載入器的預設父類載入器,系統類載入器是java類,是java.lang.ClassLoader類的子類。

類載入器之間的關係可用如圖表示:

自定義類載入器

使用者可以自定義類載入器,自定義類載入器只需要繼承java.lang.ClassLoader,重寫父類的findClass方法即可完成自定義類載入器的編寫。

我們自己動手寫一個類載入器,載入位於D盤根目錄下的類:

public class MyClassLoader extends ClassLoader {

    private final String dir = "D:/";

    //重寫父類findClass方法
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //Java包是以目錄形式存在在磁碟中,所以我們需要將.替換成/
        String className = name.replace(".", "/");
        File classFile = new File(dir, className + ".class");
        if (!classFile.exists()) {
            throw new ClassNotFoundException();
        }
        //將類讀成位元組陣列
        byte[] classBytes = loadClassBytes(classFile);
        if (classBytes == null || classBytes.length == 0) {
            throw new ClassNotFoundException();
        }
        return this.defineClass(name, classBytes, 0, classBytes.length);
    }

    private byte[] loadClassBytes(File classFile) {
        //JDK7語法糖,try with resource語法,可以不用手動的關閉資源
        try (ByteArrayOutputStream bout = new ByteArrayOutputStream();
             FileInputStream fin = new FileInputStream(classFile)) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = fin.read(buffer)) != -1) {
                bout.write(buffer, 0, len);
            }
            bout.flush();
            return bout.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

新建一個HelloWorld類編譯後放在D盤下:

public class HelloWorld {
    public String hello() {
        return "Hello World!";
    }
}

測試我們的類載入器:

public class ClassLoaderTest {

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        MyClassLoader classLoader = new MyClassLoader();
        Class<?> clazz = classLoader.loadClass("com.satra.classloader.HelloWorld");
        System.out.println(clazz);
        System.out.println(clazz.getClassLoader());
        Object obj = clazz.newInstance();
        Method method = clazz.getMethod("hello", null);
        Object o = method.invoke(obj, null);
        System.out.println(o);
    }
}

輸出結果:

類載入器雙親委派模型

前面我們說過,同一個class檔案用兩個不同的類載入器載入並建立兩個java.lang.Class物件,即使兩個物件來源自同一個class檔案,它們也是不相等的。例如當我們自定義的類載入器載入了java.lang包中的String類,這樣會造成記憶體中存在兩個String的Class物件,而這兩個Class物件的例項物件的eques會不相等,對虛擬機器的穩定執行造成危害。所以虛擬機器定義了雙親委派模型來解決這個問題。

雙親委派模型是指:某個特定的類載入器在接到載入類的請求時,首先將載入任務委託給父類載入器,依次遞迴,如果父類載入器可以完成類載入任務,就成功返回;只有父類載入器無法完成此載入任務時,才自己去載入。

我們可以看下JDK類載入器原始碼的實現:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //首先看這個類載入器是否有父類
                    if (parent != null) {
                        //有父類交由父類去載入
                        c = parent.loadClass(name, false);
                    } else {
                        //沒有父類說明這個類載入器是Bootstrap類載入器呼叫本地方法去載入
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }
                //有父類且父類不能完成載入或者沒有父類時,呼叫findClass完成載入
                if (c == null) {
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

參考文獻

《深入理解Java虛擬機器:JVM高階特性與最佳實踐》 周志明著