熱加載:在不停止程序運行的情況下,對類(對象)的動態替換
Java ClassLoader 簡述
Java中的類從被加載到內存中到卸載出內存爲止,一共經歷了七個階段:加載、驗證、準備、解析、初始化、使用、卸載。
接下來我們重點講解加載和初始化這兩步
加載
在加載的階段,虛擬機需要完成以下三件事:
- 通過一個類的全限定名來獲取定義此類的二進制字節流
- 將這個字節流所代表的的靜態存儲結構轉化爲方法區的運行時數據結構
- 在內存中生成一個代表這個類的
java.lang.Class
對象,作爲方法區這個類的各種數據的訪問入口。
這三步都是通過類加載器來實現的。而官方定義的Java類加載器有BootstrapClassLoader
、ExtClassLoader
、AppClassLoader
。這三個類加載器分別負責加載不同路徑的類的加載。並形成一個父子結構。
類加載器名稱 | 負責加載目錄 |
---|---|
BootstrapClassLoader |
處於類加載器層次結構的最高層,負責 sun.boot.class.path 路徑下類的加載,默認爲 jre/lib 目錄下的核心 API 或 -Xbootclasspath 選項指定的 jar 包 |
ExtClassLoader |
加載路徑爲 java.ext.dirs,默認爲 jre/lib/ext 目錄或者 -Djava.ext.dirs 指定目錄下的 jar 包加載 |
AppClassLoader |
加載路徑爲 java.class.path,默認爲環境變量 CLASSPATH 中設定的值。也可以通過 -classpath 選型進行指定 |
默認情況下,例如我們使用關鍵字
new
或者Class.forName
都是通過AppClassLoader
類加載器來加載的
正因爲是此父子結構,所以默認情況下如果要加載一個類,會優先將此類交給其父類進行加載(直到頂層的BootstrapClassLoader
也沒有),如果父類都沒有,那麼纔會將此類交給子類加載。這就是類加載器的雙親委派規則。
初始化
當我們要使用一個類的執行方法或者屬性時,類必須是加載到內存中並且完成初始化的。那麼類是什麼時候被初始化的呢?有以下幾種情況
- 使用new關鍵字實例化對象的時候、讀取或者設置一個類的靜態字段、以及調用一個類的靜態方法。
- 使用
java.lang.reflect
包的方法對類進行反射調用時,如果類沒有進行初始化,那麼先進行初始化。 - 初始化一個類的時候,如果發現其父類沒有進行初始化,則先觸發父類的初始化。
- 當虛擬機啓動時,用戶需要制定一個執行的主類(包含main()方法的那個類)虛擬機會先初始化這個主類。
如何實現熱加載?
在上面我們知道了在默認情況下,類加載器是遵循雙親委派規則的。所以我們要實現熱加載,那麼我們需要加載的那些類就不能交給系統加載器來完成。所以我們要自定義類加載器來寫我們自己的規則。
實現自己的類加載器
要想實現自己的類加載器,只需要繼承ClassLoader
類即可。而我們要打破雙親委派規則,那麼我們就必須要重寫loadClass
方法,因爲默認情況下loadClass
方法是遵循雙親委派的規則的。
public class CustomClassLoader extends ClassLoader{
private static final String CLASS_FILE_SUFFIX = ".class";
//AppClassLoader的父類加載器
private ClassLoader extClassLoader;
public CustomClassLoader(){
ClassLoader j = String.class.getClassLoader();
if (j == null) {
j = getSystemClassLoader();
while (j.getParent() != null) {
j = j.getParent();
}
}
this.extClassLoader = j ;
}
protected Class<?> loadClass(String name, boolean resolve){
Class cls = null;
cls = findLoadedClass(name);
if (cls != null){
return cls;
}
//獲取ExtClassLoader
ClassLoader extClassLoader = getExtClassLoader() ;
//確保自定義的類不會覆蓋Java的核心類
try {
cls = extClassLoader.loadClass(name);
if (cls != null){
return cls;
}
}catch (ClassNotFoundException e ){
}
cls = findClass(name);
return cls;
}
@Override
public Class<?> findClass(String name) {
byte[] bt = loadClassData(name);
return defineClass(name, bt, 0, bt.length);
}
private byte[] loadClassData(String className) {
// 讀取Class文件呢
InputStream is = getClass().getClassLoader().getResourceAsStream(className.replace(".", "/")+CLASS_FILE_SUFFIX);
ByteArrayOutputStream byteSt = new ByteArrayOutputStream();
// 寫入byteStream
int len =0;
try {
while((len=is.read())!=-1){
byteSt.write(len);
}
} catch (IOException e) {
e.printStackTrace();
}
// 轉換爲數組
return byteSt.toByteArray();
}
public ClassLoader getExtClassLoader(){
return extClassLoader;
}
}
爲什麼要先獲取ExtClassLoader
類加載器呢?其實這裏是借鑑了Tomcat裏面的設計,是爲了避免我們自定義的類加載器覆蓋了一些核心類。例如java.lang.Object
。
爲什麼是獲取ExtClassLoader
類加載器而不是獲取AppClassLoader
呢?這是因爲如果我們獲取了AppClassLoader
進行加載,那麼不還是雙親委派的規則了嗎?
監控class文件
這裏我們使用ScheduledThreadPoolExecutor
來進行週期性的監控文件是否修改。在程序啓動的時候記錄文件的最後修改時間。隨後週期性的查看文件的最後修改時間是否改動。如果改動了那麼就重新生成類加載器進行替換。這樣新的文件就被加載進內存中了。
首先我們建立一個需要監控的文件:
public class Test {
public void test(){
System.out.println("Hello World! Version one");
}
}
我們通過在程序運行時修改版本號,來動態的輸出版本號。
接下來我們建立週期性執行的任務類。
public class WatchDog implements Runnable{
private Map<String,FileDefine> fileDefineMap;
public WatchDog(Map<String,FileDefine> fileDefineMap){
this.fileDefineMap = fileDefineMap;
}
@Override
public void run() {
File file = new File(FileDefine.WATCH_PACKAGE);
File[] files = file.listFiles();
for (File watchFile : files){
long newTime = watchFile.lastModified();
FileDefine fileDefine = fileDefineMap.get(watchFile.getName());
long oldTime = fileDefine.getLastDefine();
//如果文件被修改了,那麼重新生成累加載器加載新文件
if (newTime!=oldTime){
fileDefine.setLastDefine(newTime);
loadMyClass();
}
}
}
public void loadMyClass(){
try {
CustomClassLoader customClassLoader = new CustomClassLoader();
Class<?> cls = customClassLoader.loadClass("com.example.watchfile.Test",false);
Object test = cls.newInstance();
Method method = cls.getMethod("test");
method.invoke(test);
}catch (Exception e){
System.out.println(e);
}
}
}
可以看到在上面的gif演示圖中我們簡單的實現了熱加載的功能。
優化
在上面的方法調用中我們是使用了getMethod()
方法來調用的。此時或許會有疑問,爲什麼不直接將newInstance()
強轉爲Test
類呢?
如果我們使用了強轉的話,代碼會變成這樣Test test = (Test) cls.newInstance()
。但是在運行的時候會拋ClassCastException
異常。這是爲什麼呢?因爲在Java中確定兩個類是否相等,除了看他們兩個類文件是否相同以外還會看他們的類加載器是否相同。所以即使是同一個類文件,如果是兩個不同的類加載器來加載的,那麼它們的類型就是不同的。
WatchDog
類是由我們new出來的。所以默認是AppClassLoader
來加載的。所以test
變量的聲明類型是WatchDog
方法中的一個屬性,所以也是由AppClassLoader
來加載的。因此兩個類不相同。
該如何解決呢?問題就出在了=
號雙方的類不一樣,那麼我們給它搞成一樣不就行了嗎?怎麼搞?答案就是接口。默認情況下,如果我們實現了一個接口,那麼此接口一般都是以子類的加載器爲主的。意思就是如果沒有特殊要求的話,例如A implements B
如果A的加載器是自定義的。那麼B接口的加載器也是和子類是一樣的。
所以我們要將接口的類加載器搞成是AppClassLoader
來加載。
所以自定義加載器中加入這一句:
if ("com.example.watchfile.ITest".equals(name)){
try {
cls = getSystemClassLoader().loadClass(name);
} catch (ClassNotFoundException e) {
}
return cls;
}
建立接口:
public interface ITest {
void test();
}
這樣我們就能愉快的調用了。直接調用其方法。不會拋異常,因爲=
號雙方的類是一樣的。
CustomClassLoader customClassLoader = new CustomClassLoader();
Class<?> cls = customClassLoader.loadClass("com.example.watchfile.Test",false);
ITest test = (ITest) cls.newInstance();
test.test();
參考文章:
- https://www.ibm.com/developerworks/cn/java/j-lo-hotswapcls/index.html
- https://www.jianshu.com/p/d8fa14802b7a
作者:不學無數的程序員
鏈接:https://www.jianshu.com/p/d8fa14802b7a
近期熱文推薦:
1.600+ 道 Java面試題及答案整理(2021最新版)
2.終於靠開源項目弄到 IntelliJ IDEA 激活碼了,真香!
3.阿里 Mock 工具正式開源,幹掉市面上所有 Mock 工具!
4.Spring Cloud 2020.0.0 正式發佈,全新顛覆性版本!
覺得不錯,別忘了隨手點贊+轉發哦!