JVM(六):探究類載入過程-下

iceWang丶發表於2019-06-28

JVM(六):探究類載入過程-下

上文說了類載入過程的5個階段,著重介紹了各個階段做的工作。在本文中,我們對執行載入階段的主體進行探討,學習類載入器的模型和邏輯,以及我們該如何自定義一個類載入器。

定義

前面說過載入階段是一個可以讓設計人員高度自控的模組,因為類檔案的源頭可以是多種多樣的,程式碼生成、反射生成或從網路中生成等。因此類載入器作為對這些檔案的處理就顯得尤為重要。

但類載入器的功能不僅如此,其還有一個重要的功能就是和一個類的全限定名唯一確定一個類。通俗來說,要說兩個類是相同的,不僅其全限定名要一樣,其對應的類載入器也必須相同,才能說明兩個類是相等的。

正因為類載入器的功能角色如此重要,因此虛擬機器對其的實現規範也十分重視。在Java虛擬機器中,對其的實現模型是雙親委派模型

模型

雙親委派模型

雙親委派模型的主要執行過程示意圖如上所示,其分為啟動類載入器(Bootstrap Class-loader),擴充類載入器(Extension Class-loader),應用程式類載入(Application Class-loader)。

其中啟動類載入器主要負責載入 JRE 的核心類庫,如 JRE 目錄下的 rt.jar。但其實根據《深入分析 Java Web 技術內幕》上所說,啟動類載入器並不嚴格符合雙親委派模型,因為Bootstrap Class-loader 並不屬於 JVM 的類等級層次。Bootstrap Class-loader 是沒有子類的,Extension Class-loader 也是沒有父類的。不過在這裡我們並不深究,只要知道有這一點就可以了。

Extension Class-loader 主要負責載入 JRE 擴充目錄 ext 下的類。

Application Class-loader 主要負責使用者類路徑(Class-path)下的類,這個類載入器是使用的最多的,因為大大多數情況下,一般開發者並沒有實現自定義的類載入器,那麼 JVM 就會使用這個來載入類大部分類。

執行過程

雙親委派模型執行過程

上圖就是雙親委派模型的執行過程,當類開始載入的時候,先檢查是否已經被載入過,如果沒有被載入過,則呼叫父類的載入方法,如果父類載入失敗,丟擲異常,則呼叫自身的 findClass() 方法進行載入。

JDK 中載入過程的原始碼分析:

protected Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First,check if the class has already been loaded
        // 如果載入過了,就不要載入直接返回
        Class c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 判斷是否有父載入器
                if (parent != null) {
                    // 有父載入器則呼叫父載入器載入
                    c = parent.loadClass(name, false);
                } else {
                    // 無父親就呼叫 bootstarp 載入器來載入
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
        // 父載入器和 bootstarp 載入器都沒有找到指定類,呼叫當前類的 findClass() 來完成類載入
        // 因此,自定義類載入器,就是重寫 findClass() 方法
        if (c == null) {
            // If still not found, then invoke findClass in order
            // to find the class.
            long t1 = System.nanoTime();
            c = findClass(name);
           // this is the defining class loader; record the stats
            sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
            sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
            sun.misc.PerfCounter.getFindClasses().increment();
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}
}

從原始碼中,我們可以看到其實符合規範要求的 雙親委派模型 的。而當我們要自定義一個類載入器的時候就是通過重寫 findClass() 來實現的。

自定義類載入器

/**
 * 1. 自定義類載入器通過整合ClassLoader來實現,主要通過重寫findClass方法
 * 2. findClass方法首先通過自定義的loadByte()方法將Class檔案轉換成byte[]位元組流
 * 3. 然後通過defineClass()方法將其轉換為Class物件
 */
public class SelfClassLoader extends ClassLoader {
    private String classPath;
    public SelfClassLoader(String classPath) {
        this.classPath = classPath;
    }

    /**
     * 通過 difineClass,將一個位元組陣列轉換為Class 物件
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = loadByte(name);
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }

    /**
     * 根據路徑將指定的檔案讀取為byte 流
     * @param name
     * @return
     * @throws IOException
     */
    private byte[] loadByte(String name) throws IOException {
        name = name.replaceAll("\\.", "/");
        FileInputStream fis = new FileInputStream(classPath + "/" + name
                + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }
}

另一個種實現自定義類載入器的方法:

/**
 * 1. 載入指定packageName下的類
 * 2. 用自定義類載入器進行載入,如果載入失敗,再交給父載入器進行載入
 */
public class UrlSelfClassloader extends URLClassLoader {
    private String packageName = "";

public UrlSelfClassloader(URL[] urls, ClassLoader parent) {
    super(urls, parent);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    Class<?> aClass = findLoadedClass(name);
    if (Objects.nonNull(aClass)){
        return aClass;
    }
    if (!packageName.startsWith(name)){
        return super.loadClass(name);
    }else {
        return findClass(name);
    }
}
}

如何使用自定義的類載入器

public static void main(String args[]) throws Exception {
    MyClassLoader classLoader = new MyClassLoader("");
    Class clazz = classLoader.loadClass("");
    Object obj = clazz.newInstance();
    Method helloMethod = clazz.getDeclaredMethod("hello", null);
    helloMethod.invoke(obj, null);
}

總結

在本文中,我們講解了類載入器的實現模型,分析了在 JDK 中類載入器的原始碼實現,並根據原始碼中的程式碼實現,自定義了一個類載入器的實現。

此外相信經過五和六兩篇文章的學習,大家應該對如何將類載入入虛擬機器中有了系統的理解。

後面的文章中,我們就要進入 JVM 的內部了,從下篇文章開始,我們就開始逐步講解 JVM 的記憶體佈局,瞭解 JVM 中的各個邏輯上劃分的儲存結構以及其作用,歡迎各位讀者瀏覽。

iceWang公眾號

文章在公眾號「iceWang」第一手更新,有興趣的朋友可以關注公眾號,第一時間看到筆者分享的各項知識點,謝謝!筆芯!

本系列文章主要借鑑《深入分析 Java Web 技術內幕》和《深入理解 Java 虛擬機器 - JVM 高階特性與最佳實踐》。

相關文章