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