Arthas 是由阿里巴巴開源實現的一套 Java 診斷工具,能夠實現對 Java 進程全方位的診斷與調試。其豐富實用的命令選項使得其深受 Java 開發工程師的喜愛。
在 Arthas 中有一個命令可以實現部分代碼的熱部署功能,這里介紹一下它具體是怎么實現的。
熱部署概念
熱部署在近些年來在 Java 組成的應用十分受歡迎。主要源於隨着項目的越來越大,Java 程序的啟動需要加載大量的內容,導致啟動時間十分耗時。對於線上應用程序而言,相較於程序的運行時長,啟動時長幾乎可以忽略不計。而對於 Java 開發者來說,由於需要對代碼進行調試,頻繁的啟動應用所帶來的啟動耗時便成了負擔。這時候熱部署便應運而生了。
代碼的熱部署表示在在不重啟應用的情況下,只將代碼修改的部分替換到正在運行的程序中,從而實現動態代碼修改的效果。這樣就不需要開發者只改一行代碼也要重新啟動項目了。節省下來的重啟時間可以專注於業務代碼的開發過程,從而提升開發效率。
熱部署方案
目前市面上主流的熱部署解決方案有 JRebel,HotSwapAgent等,如果開發者使用的是 Spirng Boot 的話可以試試 spring-boot-devtools。
今天要介紹的是 Arthas 中的熱部署命令redefine,對於熱部署解決方案,Arthas 使用了另一種解決方案實現,這里就要講下 JVMTI 了。
JVMTI 介紹
JVMTI(JVM Tool Interface)是 Java 虛擬機所提供的 native 編程接口,是 JVMPI(Java Virtual Machine Profiler Interface)和 JVMDI(Java Virtual Machine Debug Interface)的替代版本。
JVMTI可以用來開發並監控虛擬機,可以查看JVM內部的狀態,並控制JVM應用程序的執行。可實現的功能包括但不限於:調試、監控、線程分析、覆蓋率分析工具等。
Arthas 的熱部署的實現就是使用了 JVMTI 中的 Instrument
接口,通過該接口可以實現字節碼的動態替換。
Instrumention
支持的功能都在java.lang.instrument.Instrumentation
接口中體現
public interface Instrumentation {
//添加一個ClassFileTransformer
//之后類加載時都會經過這個ClassFileTransformer轉換
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void addTransformer(ClassFileTransformer transformer);
//移除ClassFileTransformer
boolean removeTransformer(ClassFileTransformer transformer);
boolean isRetransformClassesSupported();
//將一些已經加載過的類重新拿出來經過注冊好的ClassFileTransformer轉換
//retransformation可以修改方法體,但是不能變更方法簽名、增加和刪除方法/類的成員屬性
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
// 判斷是否支持重新定義類
boolean isRedefineClassesSupported();
//重新定義某個類
void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException;
boolean isModifiableClass(Class<?> theClass);
// 獲取全部已經加載的類
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
@SuppressWarnings("rawtypes")
Class[] getInitiatedClasses(ClassLoader loader);
// 獲取對象大小
long getObjectSize(Object objectToSize);
// 添加到啟動類加載器搜索路徑中
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
// 添加到系統類夾雜器搜索路徑中
void appendToSystemClassLoaderSearch(JarFile jarfile);
boolean isNativeMethodPrefixSupported();
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}
關鍵詞 JVMTI
我們通過addTransformer方法注冊了一個ClassFileTransformer,后面類加載的時候都會經過這個Transformer處理。對於已加載過的類,可以調用retransformClasses來重新觸發這個Transformer的轉換。
redefineClasses 和 retransformClasses 的區別:
transform是對類的byte流進行讀取轉換的過程,需要先獲取類的byte流然后做修改。而redefineClasses更簡單粗暴一些,它需要直接給出新的類byte流,然后替換舊的。
transform可以添加很多個,retransformClasses
可以讓指定的類重新經過這些transform做轉換。
有了上面的工具,Arthas 就擁有了代碼熱部署的能力了,我們翻下 arthas 的源碼看看它是怎么做的:
// 以下代碼存在刪減,如需
// 創建一個新的熱部署模型
RedefineModel redefineModel = new RedefineModel();
// 獲取 JVMTI 中的 Instrumentation 對象,來執行 redefineClasses 實現熱部署
Instrumentation inst = process.session().getInstrumentation();
// 讀取文件路徑
// ...
List<ClassDefinition> definitions = new ArrayList<ClassDefinition>();
// Instrumentation inst 獲取全部加載的類
for (Class<?> clazz : inst.getAllLoadedClasses()) {
if (bytesMap.containsKey(clazz.getName())) {
// 如果包含則執行熱部署
if (hashCode == null && classLoaderClass != null) {
// 獲取匹配的類加載器
List<ClassLoader> matchedClassLoaders = ClassLoaderUtils.getClassLoaderByClassName(inst, classLoaderClass);
if (matchedClassLoaders.size() == 1) {
// 設置hashcode
hashCode = Integer.toHexString(matchedClassLoaders.get(0).hashCode());
} else if (matchedClassLoaders.size() > 1) {
// 如果匹配多個,則拋出異常
//...
} else {
process.end(-1, "Can not find classloader by class name: " + classLoaderClass + ".");
return;
}
}
// 獲取對應的類加載器
ClassLoader classLoader = clazz.getClassLoader();
// 類定義列表
definitions.add(new ClassDefinition(clazz, bytesMap.get(clazz.getName())));
redefineModel.addRedefineClass(clazz.getName());
}
}
try {
// 獲取 JVMTI 中的 Instrumentation 對象,來執行 redefineClasses 實現熱部署
inst.redefineClasses(definitions.toArray(new ClassDefinition[0]));
process.appendResult(redefineModel);
process.end();
} catch (Throwable e) {
String message = "redefine error! " + e.toString();
logger.error(message, e);
process.end(-1, message);
}
而 Instrumentation
又是怎么實現熱部署的呢?通過翻閱 Instrumentation
的實現我們可以看到 redefineClasses
等方法的實現都是通過 JNI
實現的,因此就需要我們去翻閱一下 JDK 源碼了。到目前為止,我們可以得到 Arthas 實現熱部署是通過 JDK 的工具類 Instrumentation
實現的,通過傳入對應的類對象,和修改后的字節碼,便可以實現對目標類的字節碼進行調整替換了。
Instrumentation
通過調用 src\hotspot\share\prims\jvmtiRedefineClasses.cpp中的 VM_RedefineClasses
類,通過 虛擬機線程對目標類常量池中對應的符號進行了一一替換:
通過 Instrumentation
實現熱部署的一些限制
並不是所有改動熱更新都將會成功,當前使用 Instrumentation#redefineClasses 還是存在一些限制。從源碼可以看到,我們只是對字節碼中已有的一些內容進行了替換,因此無法添加,刪除方法或字段,也不能更改方法的簽名或繼承關系。
參考資料
Java JVMTI和Instrumention機制介紹
open-jdk
RedefineCommand
手把手教你實現熱更新功能,帶你了解 Arthas 熱更新背后的原理
熱更新原理及實踐注意