摘自《Java高併發編程詳解-多線程架構與設計》第九章 p158-p176
文章目錄
總結
-
內置類加載器 bootstrap ClassLoader,Ext ClassLoader ,App ClassLoader。分別加載jre\lib,jre\lib\ext,-cp或者-classpath對應的classpath
-
通過繼承ClassLoader重寫findClass特殊目錄來實現自定義類加載器。特殊目錄或者設置父加載器爲空,讓loadClass時跳過父類加載器(ps,如父類已經加載同名類,父加載器非空則會返回cache)。重寫loadClass可以完全繞過雙親委託。
-
class的實例是被類加載器的【實例】隔離。(當然不同的類加載器類型也會隔離) 因此代碼中儘量得到同一個classLoader的實例,避免拿不到緩存,反覆 findClass+defineClass。可以設置線程上下文類加載器。
-
在不指定parent的情況下,自定義ClassLoader的parent AppClassLoader是指的應用中唯一的AppClassLoader。因此加載classpath下已被加載過的類時,會因調用ClassLoader#getSystemClassLoader()對應AppClassLoader實例去調用
findLoadedClass
去獲取Class緩存
ps:loadClass來觀察,而不是使用Class.forName來觀察。Class.forName在loadClass之前應該還有一次獲取緩存的機會。
1.內置三大類加載器
1.1 根加載器 Boostrap ClassLoader
C++編寫
-Xbootclasspath指定根加載器的路徑。
sun.boot.class.path獲得跟加載器加載的資源
jie\lib
1.2 擴展類加載器 Ext ClassLoader
Java編寫,URLClassLoader的子類
用於加載 JAVA_HOME下的jre\lib\ext 裏面的類庫
java.ext.dirs可以獲得擴展類加載器加載的類
jre\lib\ext
也可以將自己的類放到擴展類加載器的位置
1.3 系統類加載器 App ClassLoader
負責加載 -cp/-classpath 指定的類庫資源
2.自定義類加載器
要點
-
自定義的類加載器都是ClassLoader的直接或間接在子類
-
需要重寫抽象ClassLoader的 findClass方法。
-
需要指定一個父類類加載器。若不指定則繞過了雙親委派
-
需要自定義一個路徑加載特殊的class,該目錄不能爲已經有類加載器使用過的目錄。
可以使用loadClass打破雙親委派後,再使用任意路徑。(相同的目錄導致被委託給了父類類加載器加載) -
得到類的二進制數據(無論網絡/本地讀取或動態代理/cglib生存),使用defineClass將其變成Class
案例
/**
* @auth thewindkee
* @date 2018/12/15 0015 21:58
*/
public class MyClassLoader extends ClassLoader {
//public static final String LIB_PATH = "C:\\Users\\gkwind\\Desktop";
// 默認加載的位置
public static final String LIB_PATH = "E:\\gkrep\\gitee\\test\\other\\src\\main\\java\\com\\gkwind\\ClassLoader\\demo\\";
/*
* !不要定義在bootstrap,ext,app類加載器能加載的位置。
* 因爲雙親委派的關係,會導致該類加載器失效。
* 如下定義必須修改loadClass打破雙親委派
*/
//public static final String LIB_PATH = MyClassLoader.class.getResource("/").getPath();
public MyClassLoader() {
//設置爲null可以繞過雙親委派
//super(null);
}
public MyClassLoader(ClassLoader parent) {
super(parent);
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
//這裏查找多層
byte[] bytes = getClassBytes(name);
//defineClass方法可以把二進制流字節組成的文件轉換爲一個java.lang.Class
Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
System.out.println(c);
return c;
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
private byte[] getClassBytes(String name) throws Exception {
// 這裏要讀入.class的字節,因此要使用字節流
String resolvedPath = name.replace(".", "/");
File file = new File(LIB_PATH + resolvedPath + ".class");
System.out.println(MyClassLoader.class.getName() + "findClass:" + file);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//FileInputStream fis = new FileInputStream(file);
//FileChannel fc = fis.getChannel();
//WritableByteChannel wbc = Channels.newChannel(baos);
//ByteBuffer by = ByteBuffer.allocate(1024);
//while (true) {
// int i = fc.read(by);
// if (i == 0 || i == -1)
// break;
// by.flip();
// wbc.write(by);
// by.clear();
//}
//fis.close();
//wbc.close();
java.nio.file.Files.copy(file.toPath(), baos);
return baos.toByteArray();
}
}
自定義的類加載器指定特殊目錄或者指定父類加載器爲空,避免雙親委派導致該類加載器失效
classpath 不含有Demo.class
選擇父類ClassLoader未加載的目錄
2.2 雙親委託機制
爲保證基礎類不被破壞。加載類的時候優先祖師爺(遞歸父類)過目(加載)。 ----by 極客時間 jvm
loadClass 規定了雙親委託,所以可以直接重寫loadClass打破雙親委託。
該方法是同步方法 – synchronized (getClassLoadingLock(name))
優先返回已經加載過的同名class --findLoadedClass(name)
loadClass(name,false)
false 指的是不做【連接(linked)階段】的操作。這就是爲什麼加載類,導致類的初始化(【準備階段】)。
★3.繞過雙親委託
不需要刪除class.
1,2都是需要父類未加載過同名類,否則返回cache。3可以重複加載同名類
-
繞過系統類加載器,直接使用ext類加載器作爲父類,並傳入特殊的目錄的class
無cache且父類加載不到特殊目錄,自動到子類去加載。 -
設置父類類加載器爲null
無cache且沒有父類,自動到子類去加載 -
重寫loadClass
loadClass中不再請求父類去加載
4.類加載命名空間、運行時包、類的卸載
★類加載器命名空間
類加載器作爲一個命名空間,可以隔離class
使用不同類加載器/或者同一加載器的不同實例去加載同一個類,會產生多個實例。
簡單來說,class實例被不同的classLoader實例隔離。
使用loadClass來觀察 獲取緩存
運行時包
classloader名+全路徑列明
初始類加載器
一個類的初始類加載器,包含嘗試過加載的所有父類
★測試類加載器隔離同名類-不同的類加載器實現隔離
此處使用了不同的類加載器 去隔離。
Demo.java
不同的位置的Demo.java輸出的內容不同
MyClassLoader對應的java文件
MyClassLoader2對應的另一個class
demo1與demo2中的class 放置的位置
MyClassLoader.java
注意加載class的位置不同!
MyClassLoader2.java
測試類
兩個com.gkwind.Demo都被加載成功
類加載器的實例隔離Class對象
驗證:【同一類加載器的不同實例 產生不同Class 實例】
編寫代碼證明
MyClassLoader 繼承ClassLoader
已知
1.Demo.class是自己編譯的Class,不存在於
lib,lib/ext,classpath.在目錄X下
2.A類對應A.class編譯在classpath下。
3.MyClassLoader繼承了ClassLoader,自定義了 findClass的目錄X(App,Ext類加載器無法加載X目錄),沒有重寫loadClass,更沒有打破雙親委派。
問題:爲何MyClassLoader加載Demo的時候,無法通過findLoadedClass獲取Class的Cache?
加載A類,注意看兩次new MyClassLoader的parent都是同一個實例。
演示時,造成A類與Demo類不一樣的原因是:classpath對應的AppClassLoader 全局唯一。
-
new MyClassLoader().loadClass(“A”)多次,
委託parent->唯一的AppClassLoader去加載,第一次findClass,第二次findLoadedClass -
new MyClassLoader().loadClass(“Demo”)多次,由於Demo.class不存在classpath對應的目錄,導致由new MyClassLoader()實例去加載,每次都是新的ClassLoader實例,因此無法從findLoadedClass中獲得緩存的Demo.class
結論:
- 儘量保持ClassLoader的唯一,避免不同ClassLoader實例重複加載Class。
- 可以通過線程去傳遞ClassLoader。
- 【同一類加載器的不同實例 產生不同Class 實例】 正確。—由加載Demo類可以看出。
- 繼承ClassLoader的自定義類加載器默認會調用super()傳入默認的AppClassLoader作爲parent。super()傳入唯一的AppClassLoader