自定義ClassLoader

早知今日發表於2020-05-28

簡單地純粹地記錄下如何進行自定義一個自己的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區都有一個自己的一畝三分地,這樣在分配記憶體時,不需要加鎖做同步,提升分配的效率。

相關文章