自定義ClassLoader

簡單地純粹地記錄下如何進行自定義一個自己的ClassLoader

什麼雙親委派模型啊,雙親委派模型的破壞啊,好處啊,缺點啊什麼的,一概不說。

自定義ClassLoader的博客啥的,看過不少,但是就是沒自己親手寫一下,今天嘗試寫一下,發現古人誠不欺我!

紙上得來終覺淺,絕知此事要躬行

失敗版本

最開始是這麼寫的

public class MyClassLoader extends ClassLoader {

    @Override
    protected Class findClass (String name) throws ClassNotFoundException {

        String classPath = name.replace(".", "/");
        InputStream classInputStream = getSystemClassLoader().getResourceAsStream(classPath);
        try {
            byte[] classBytes = new byte[classInputStream.available()];
            classInputStream.read(classBytes);
            Class clazz = defineClass(name, classBytes, 0, classBytes.length);
            resolveClass(clazz);
            return clazz;
        } catch (IOException e) {
            throw new ClassNotFoundException();
        }

    }
}

這裡錯誤比較多,不過還是記住了一個,我是重寫了findClass方法,而不是重寫了loadClass方法,推薦也是通過重寫findClass方法,以前是重寫loadClass方法的方式。

即使是錯誤的,但是寫之前還是絞盡腦汁的想了好久,試圖把記憶中那點破碎的,分崩離析而又即將消失的關於自定義ClassLoader的記憶,給重新恢復了。可惜的是,我並不具體這個能力,憑着那點僅存的記憶,寫下我的第一個自定義ClassLoader,很遺憾它是錯誤的。

寫完後,就去測試跑了下,發現並沒有出現我期許的結果 。

這裡說下期許的結果是什麼

  1. 加載class文件後生成的Class對象,調用其getClassLoader方法,應該是輸出MyClassLoader
  2. Class對象和使用系統類加載器加載的同一個class代表的Class對象,並不相等,==會返回false
  3. 自定義類加載器加載的對象,是沒辦法強轉成系統類加載器加載的Class類型。

然後,沒有一個結果符合預期的。

看到輸出的ClassLoader還是AppClassLoader,很奇怪,我明明自定義了類加載還去加載了啊!

最終發現,直接繼承ClassLoader時,使用默認的無參構造

protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}

默認情況下,繼承自ClassLoader的子類,會擁有一個父類加載,就是 AppClassLoader,而 要加載的類 ,發現已經被父類加載器加載過了,所以實際上並沒有子類的findClass方法

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 {
                  	// 不存在父加載器,使用啟動類加載器去加載 
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            }
	    // 如果以上都找不到,就使用下面的邏輯去查找 
            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();
            }
        }
      	// 上面僅僅完成了一個加載class的動作,但是整個類的加載並沒有完成
        // 如果需要解析,則會對Class對象進行解析,這個名字有誤導性,其實這是類加載階段的鏈接階段 
        // 也就是 驗證 準備 解析三個階段
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

所以問題就很明了了,

第一次修改後的版本

public class MyClassLoader extends ClassLoader {
    public MyClassLoader () {
      // 不使用系統類加載器作為此類加載的父加載器
      // 這樣它的父加載器就是啟動類加載器
      super(null);
    }
    @Override
    protected Class findClass (String name) throws ClassNotFoundException {

        String classPath = name.replace(".", "/");
        InputStream classInputStream = getSystemClassLoader().getResourceAsStream(classPath);
        try {
            byte[] classBytes = new byte[classInputStream.available()];
            classInputStream.read(classBytes);
            Class clazz = defineClass(name, classBytes, 0, classBytes.length);
            resolveClass(clazz);
            return clazz;
        } catch (IOException e) {
            throw new ClassNotFoundException();
        }

    }
}

這個一跑,也是完蛋,不過好解決。

一般調用loadClass方法時,傳的都是包名,這裡是要去加載位元組碼的,也就是找class文件,所以要轉換成具體的路徑,這裡的路徑使用的是相對路徑,類位於classpath目錄下,所以直接使用ClassLoader#getResourceAsStream就可以獲取class文件的位元組流`了。

這裡實現的位元組碼來源是從文件系統加載的class文件,實際上任何符合Java虛擬機規範的Class結構的位元組數組,都可以被加載進來,動態代理就是在運行時生成位元組碼,然後直接加載的。

可運行版本

public class MyClassLoader extends ClassLoader {

    public MyClassLoader () {
        super(null);

    }

    @Override
    protected Class findClass (String name) throws ClassNotFoundException {

        String classPath = name.replace(".", "/")+".class";
        InputStream classInputStream = getSystemClassLoader().getResourceAsStream(classPath);
        try {
            byte[] classBytes = new byte[classInputStream.available()];
            classInputStream.read(classBytes);
            Class clazz = defineClass(name, classBytes, 0, classBytes.length);
            resolveClass(clazz);
            return clazz;
        } catch (IOException e) {
            throw new ClassNotFoundException();
        }

    }
}

這就是一個麻雀雖小五臟俱全的自定義類加載器了。

兩個重要知識點

就想到這倆,肯定不止倆

同一個類的Class對象在同一個虛擬機進程中,可以存在多個實例,在虛擬機中,是根據Class所屬的類加載器,來確定唯一一個Class

Hotspot虛擬機在進行類加載時,採用了類似的TLAB的方式,會給每個類加載器分配一塊內存,這樣這個類加載器加載的類,直接在這裡分配,提高效率,也便於管理,不過遇到有很多類加載的話,會出現OOM的可能,原因就是每個類加載器分配一塊,多整一些 ,空間不夠了,OOM

TLAB(Thread Local Allocate Buffer),目的是提升性能的,每一個線程在新生代的Eden區都有一個自己的一畝三分地,這樣在分配內存時,不需要加鎖做同步,提升分配的效率。