簡單地純粹地記錄下如何進行自定義一個自己的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
,很遺憾它是錯誤的。
寫完後,就去測試跑了下,發現並沒有出現我期許的結果 。
這裡說下期許的結果是什麼
- 載入
class
檔案後生成的Class
物件,呼叫其getClassLoader
方法,應該是輸出MyClassLoader
的 - 此
Class
物件和使用系統類載入器載入的同一個class
代表的Class
物件,並不相等,==
會返回false
- 自定義類載入器載入的物件,是沒辦法強轉成系統類載入器載入的
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區都有一個自己的一畝三分地,這樣在分配記憶體時,不需要加鎖做同步,提升分配的效率。