《深入理解 Java 虛擬機器》閱讀筆記2系列:附錄:實現Java類的熱替換
阿新 • • 發佈:2021-01-17
實現 Java 類的熱替換
什麼是熱替換及其實現原理
- 熱替換是在不停止正在執行的系統的情況下進行類(物件)的升級替換;
- 這要求虛擬機器中要存在同一個類的兩個不同版本。可我們知道,我們是無法將同一個類載入兩遍的,想要實現這點,我們需要讓虛擬機器認為這是兩個不同的類,即用兩個不同的類載入器去載入這個類不同版本的 class 檔案;
- 因此,這個工作就不能由系統提供給我們的啟動類載入器,擴充套件類載入器或者應用程式類載入器來完成,因為這三個類載入器在同一個虛擬機器中只有一份,不僅如此,我們還要跳過這些類載入器;
- 想要跳過這些類載入器可不是隻要不用這些類載入器就行了,還需要我們跳過雙親委派模型,否則類的載入還會被委派到這些個類載入器,如果恰好某個類之前是由這三個類載入器中的一個載入的,虛擬機器就不會再次載入新版本的類了,就無法實現類的熱替換了。
實現簡單的 Java 類熱替換
需求分析
現有一 Foo 類,可以在控制檯持續列印:Hello world! version one,我們將在該類執行時,將其 .class 檔案替換為修改後的 Foo 類的 .class 檔案,修改後的 Foo 會在控制檯持續列印:Hello world! version two。也就是說,替換之後,控制檯列印的內容發生變化,就說明類的熱替換實現成功。
Foo 類的實現:
public class Foo {
public void sayHello() {
System.out.println("Hello world! version one");
// System.out.println("Hello world! version two"); // 之後替換成這個
}
}
然後我們通過如下程式執行 Foo 類:
public class Task extends TimerTask {
@Override
public void run () {
String basePath = "C:\\Users\\Bean\\IdeaProjects\\USTJ\\target\\classes";
// 每執行一次任務都 new 一個新的類載入器
HotswapClassLoader cl = new HotswapClassLoader(
basePath, new String[]{"com.jvm.ch7.hotswap.Foo"});
try {
// 通過我們自己實現的類載入器載入 Foo 類
Class cls = cl.loadClass("com.jvm.ch7.hotswap.Foo", true);
Object foo = cls.newInstance();
Method method = cls.getMethod("sayHello", new Class[]{});
method.invoke(foo, new Object[]{});
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new Task(), 0, 1000);
}
}
實現類載入器
HotswapClassLoader
的實現如下,具體的講解已被寫入註釋中:
public class HotswapClassLoader extends ClassLoader {
private String basePath;
private HashSet<String> loadedClass; // 用來記錄被這個類載入器所載入的類
public HotswapClassLoader(String basePath, String[] classList) {
// 跳過父類載入器,把它設為null
super(null);
this.basePath = basePath;
loadedClass = new HashSet<>();
// 該類載入器在初始化的時候會直接把應該它負責載入的類載入好,
// 這樣之後 loadClass 時,會在第一步檢驗該類是否已經被載入時發現該類已經被載入過了,
// 就無需執行 loadClass 之後的流程,直接返回虛擬機器中被載入好的類即可,
// 這樣雖然初始化的時間長了點,但是之後 loadClass 時會比較省時間
loadClassByMe(classList);
}
/**
* 載入給定的的 classList 中的類到虛擬機器
*/
private void loadClassByMe(String[] classList) {
for (int i = 0; i < classList.length; i++) {
Class cls = loadClassDirectly(classList[i]);
if (cls != null) {
loadedClass.add(classList[i]);
}
}
}
/**
* 通過檔名直接載入類,得到Class物件
*/
private Class loadClassDirectly(String className) {
Class cls = null;
StringBuilder sb = new StringBuilder(basePath);
String classPath = className.replace(".", File.separator) + ".class";
sb.append(File.separator + classPath);
File file = new File(sb.toString());
InputStream fin = null;
try {
fin = new FileInputStream(file);
// 將位元組流轉化成記憶體中的Class物件
cls = instantiateClass(className, fin, (int) file.length());
return cls;
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fin != null) {
try {
fin.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
/**
* 將位元組流轉化成記憶體中的Class物件啊,使用defineClass方法!
*/
private Class instantiateClass(String name, InputStream fin, int len) {
byte[] buffer = new byte[len];
try {
fin.read(buffer);
return defineClass(name, buffer, 0, len);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fin != null) {
try {
fin.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
/**
* 覆蓋原有的loadClass規則,
*/
public Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class cls = null;
// 應該由 HotswapClassLoader 負責載入的類會通過下面這一行得到類的 Class 物件,
// 因為早在 HotswapClassLoader 類載入器執行建構函式時,它們就被載入好了
cls = findLoadedClass(name);
// 只有在這個類沒有被載入,且!這個類不是當前這個類載入器負責載入的時候,才去使用啟動類載入器
if (cls == null && !loadedClass.contains(name)) {
cls = findSystemClass(name);
}
if (cls == null) {
throw new ClassNotFoundException(name);
}
// resolveClass是進行連線操作的,即"驗證+準備+解析",之後就可以進行初始化了
if (resolve) {
resolveClass(cls);
}
return cls;
}
}