JVMTM Tool Interface:JVM源碼分析之javaagent原理完全解讀
概述
本文重點(diǎn)講述javaagent的具體實(shí)現(xiàn),因?yàn)樗嫦虻氖俏覀僇ava程序員,而且agent都是用Java編寫的,不需要太多的C/C++編程基礎(chǔ),不過(guò)這篇文章里也會(huì)講到JVMTIAgent(C實(shí)現(xiàn)的),因?yàn)閖avaagent的運(yùn)行還是依賴于一個(gè)特殊的JVMTIAgent。
對(duì)于javaagent,或許大家都聽過(guò),甚至使用過(guò),常見的用法大致如下:
java -javaagent:myagent.jar=mode=test Test
通過(guò)-javaagent來(lái)指定我們編寫的agent的jar路徑(./myagent.jar),以及要傳給agent的參數(shù)(mode=test),在啟動(dòng)的時(shí)候這個(gè)agent就可以做一些我們希望的事了。
javaagent的主要功能如下:
- 可以在加載class文件之前做攔截,對(duì)字節(jié)碼做修改
- 可以在運(yùn)行期對(duì)已加載類的字節(jié)碼做變更,但是這種情況下會(huì)有很多的限制,后面會(huì)詳細(xì)說(shuō)
- 還有其他一些小眾的功能
- 獲取所有已經(jīng)加載過(guò)的類
- 獲取所有已經(jīng)初始化過(guò)的類(執(zhí)行過(guò)clinit方法,是上面的一個(gè)子集)
- 獲取某個(gè)對(duì)象的大小
- 將某個(gè)jar加入到bootstrap classpath里作為高優(yōu)先級(jí)被bootstrapClassloader加載
- 將某個(gè)jar加入到classpath里供AppClassloard去加載
- 設(shè)置某些native方法的前綴,主要在查找native方法的時(shí)候做規(guī)則匹配
想象一下可以讓程序按照我們預(yù)期的邏輯去執(zhí)行,聽起來(lái)是不是挺酷的。
JVMTI
JVMTI全稱JVM Tool Interface,是JVM暴露出來(lái)的一些供用戶擴(kuò)展的接口集合。JVMTI是基于事件驅(qū)動(dòng)的,JVM每執(zhí)行到一定的邏輯就會(huì)調(diào)用一些事件的回調(diào)接口(如果有的話),這些接口可以供開發(fā)者擴(kuò)展自己的邏輯。
比如最常見的,我們想在某個(gè)類的字節(jié)碼文件讀取之后、類定義之前修改相關(guān)的字節(jié)碼,從而使創(chuàng)建的class對(duì)象是我們修改之后的字節(jié)碼內(nèi)容,那就可以實(shí)現(xiàn)一個(gè)回調(diào)函數(shù)賦給jvmtiEnv(JVMTI的運(yùn)行時(shí),通常一個(gè)JVMTIAgent對(duì)應(yīng)一個(gè)jvmtiEnv,但是也可以對(duì)應(yīng)多個(gè))的回調(diào)方法集合里的ClassFileLoadHook,這樣在接下來(lái)的類文件加載過(guò)程中都會(huì)調(diào)用到這個(gè)函數(shù)中,大致實(shí)現(xiàn)如下:,
jvmtiEventCallbacks callbacks;
jvmtiEnv * jvmtienv = jvmti(agent);
jvmtiError jvmtierror;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.ClassFileLoadHook = &eventHandlerClassFileLoadHook;
jvmtierror = (*jvmtienv)->SetEventCallbacks( jvmtienv,
&callbacks,
sizeof(callbacks));
JVMTIAgent
JVMTIAgent其實(shí)就是一個(gè)動(dòng)態(tài)庫(kù),利用JVMTI暴露出來(lái)的一些接口來(lái)干一些我們想做、但是正常情況下又做不到的事情,不過(guò)為了和普通的動(dòng)態(tài)庫(kù)進(jìn)行區(qū)分,它一般會(huì)實(shí)現(xiàn)如下的一個(gè)或者多個(gè)函數(shù):
JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *options, void *reserved);
JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM* vm, char* options, void* reserved);
JNIEXPORT void JNICALL
Agent_OnUnload(JavaVM *vm);
- Agent_OnLoad函數(shù),如果agent是在啟動(dòng)時(shí)加載的,也就是在vm參數(shù)里通過(guò)-agentlib來(lái)指定的,那在啟動(dòng)過(guò)程中就會(huì)去執(zhí)行這個(gè)agent里的Agent_OnLoad函數(shù)。
- Agent_OnAttach函數(shù),如果agent不是在啟動(dòng)時(shí)加載的,而是我們先attach到目標(biāo)進(jìn)程上,然后給對(duì)應(yīng)的目標(biāo)進(jìn)程發(fā)送load命令來(lái)加載,則在加載過(guò)程中會(huì)調(diào)用Agent_OnAttach函數(shù)。
- Agent_OnUnload函數(shù),在agent卸載時(shí)調(diào)用,不過(guò)貌似基本上很少實(shí)現(xiàn)它。
其實(shí)我們每天都在和JVMTIAgent打交道,只是你可能沒有意識(shí)到而已,比如我們經(jīng)常使用Eclipse等工具調(diào)試Java代碼,其實(shí)就是利用JRE自帶的jdwp agent實(shí)現(xiàn)的,只是Eclipse等工具在沒讓你察覺的情況下將相關(guān)參數(shù)(類似-agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:61349)自動(dòng)加到程序啟動(dòng)參數(shù)列表里了,其中agentlib參數(shù)就用來(lái)跟要加載的agent的名字,比如這里的jdwp(不過(guò)這不是動(dòng)態(tài)庫(kù)的名字,JVM會(huì)做一些名稱上的擴(kuò)展,比如在Linux下會(huì)去找libjdwp.so的動(dòng)態(tài)庫(kù)進(jìn)行加載,也就是在名字的基礎(chǔ)上加前綴lib,再加后綴.so),接下來(lái)會(huì)跟一堆相關(guān)的參數(shù),將這些參數(shù)傳給Agent_OnLoad或者Agent_OnAttach函數(shù)里對(duì)應(yīng)的options。
javaagent
說(shuō)到j(luò)avaagent,必須要講的是一個(gè)叫做instrument的JVMTIAgent(Linux下對(duì)應(yīng)的動(dòng)態(tài)庫(kù)是libinstrument.so),因?yàn)閖avaagent功能就是它來(lái)實(shí)現(xiàn)的,另外instrument agent還有個(gè)別名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),這個(gè)名字也完全體現(xiàn)了其最本質(zhì)的功能:就是專門為Java語(yǔ)言編寫的插樁服務(wù)提供支持的。
instrument agent
instrument agent實(shí)現(xiàn)了Agent_OnLoad和Agent_OnAttach兩方法,也就是說(shuō)在使用時(shí),agent既可以在啟動(dòng)時(shí)加載,也可以在運(yùn)行時(shí)動(dòng)態(tài)加載。其中啟動(dòng)時(shí)加載還可以通過(guò)類似-javaagent:myagent.jar的方式來(lái)間接加載instrument agent,運(yùn)行時(shí)動(dòng)態(tài)加載依賴的是JVM的attach機(jī)制(JVM Attach機(jī)制實(shí)現(xiàn)),通過(guò)發(fā)送load命令來(lái)加載agent。
instrument agent的核心數(shù)據(jù)結(jié)構(gòu)如下:
struct _JPLISAgent {
JavaVM * mJVM; /* handle to the JVM */
JPLISEnvironment mNormalEnvironment; /* for every thing but retransform stuff */
JPLISEnvironment mRetransformEnvironment;/* for retransform stuff only */
jobject mInstrumentationImpl; /* handle to the Instrumentation instance */
jmethodID mPremainCaller; /* method on the InstrumentationImpl that does the premain stuff (cached to save lots of lookups) */
jmethodID mAgentmainCaller; /* method on the InstrumentationImpl for agents loaded via attach mechanism */
jmethodID mTransform; /* method on the InstrumentationImpl that does the class file transform */
jboolean mRedefineAvailable; /* cached answer to "does this agent support redefine" */
jboolean mRedefineAdded; /* indicates if can_redefine_classes capability has been added */
jboolean mNativeMethodPrefixAvailable; /* cached answer to "does this agent support prefixing" */
jboolean mNativeMethodPrefixAdded; /* indicates if can_set_native_method_prefix capability has been added */
char const * mAgentClassName; /* agent class name */
char const * mOptionsString; /* -javaagent options string */
};
struct _JPLISEnvironment {
jvmtiEnv * mJVMTIEnv; /* the JVM TI environment */
JPLISAgent * mAgent; /* corresponding agent */
jboolean mIsRetransformer; /* indicates if special environment */
};
這里解釋一下幾個(gè)重要項(xiàng):
- mNormalEnvironment:主要提供正常的類transform及redefine功能。
- mRetransformEnvironment:主要提供類retransform功能。
- mInstrumentationImpl:這個(gè)對(duì)象非常重要,也是我們Java agent和JVM進(jìn)行交互的入口,或許寫過(guò)javaagent的人在寫
premain以及agentmain方法的時(shí)候注意到了有個(gè)Instrumentation參數(shù),該參數(shù)其實(shí)就是這里的對(duì)象。 - mPremainCaller:指向
sun.instrument.InstrumentationImpl.loadClassAndCallPremain方法,如果agent是在啟動(dòng)時(shí)加載的,則該方法會(huì)被調(diào)用。 - mAgentmainCaller:指向
sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain方法,該方法在通過(guò)attach的方式動(dòng)態(tài)加載agent的時(shí)候調(diào)用。 - mTransform:指向
sun.instrument.InstrumentationImpl.transform方法。 - mAgentClassName:在我們javaagent的MANIFEST.MF里指定的
Agent-Class。 - mOptionsString:傳給agent的一些參數(shù)。
- mRedefineAvailable:是否開啟了redefine功能,在javaagent的MANIFEST.MF里設(shè)置
Can-Redefine-Classes:true。 - mNativeMethodPrefixAvailable:是否支持native方法前綴設(shè)置,同樣在javaagent的MANIFEST.MF里設(shè)置
Can-Set-Native-Method-Prefix:true。 - mIsRetransformer:如果在javaagent的MANIFEST.MF文件里定義了
Can-Retransform-Classes:true,將會(huì)設(shè)置mRetransformEnvironment的mIsRetransformer為true。
在啟動(dòng)時(shí)加載instrument agent
正如前面“概述”里提到的方式,就是啟動(dòng)時(shí)加載instrument agent,具體過(guò)程都在InvocationAdapter.c的Agent_OnLoad方法里,這里簡(jiǎn)單描述下過(guò)程:
- 創(chuàng)建并初始化JPLISAgent
- 監(jiān)聽VMInit事件,在vm初始化完成之后做下面的事情:
- 創(chuàng)建InstrumentationImpl對(duì)象
- 監(jiān)聽ClassFileLoadHook事件
- 調(diào)用InstrumentationImpl的
loadClassAndCallPremain方法,在這個(gè)方法里會(huì)調(diào)用javaagent里MANIFEST.MF里指定的Premain-Class類的premain方法
- 解析javaagent里MANIFEST.MF里的參數(shù),并根據(jù)這些參數(shù)來(lái)設(shè)置JPLISAgent里的一些內(nèi)容
在運(yùn)行時(shí)加載instrument agent
在運(yùn)行時(shí)加載的方式,大致按照下面的方式來(lái)操作:
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent(agentPath, agentArgs);
上面會(huì)通過(guò)JVM的attach機(jī)制來(lái)請(qǐng)求目標(biāo)JVM加載對(duì)應(yīng)的agent,過(guò)程大致如下:
- 創(chuàng)建并初始化JPLISAgent
- 解析javaagent里MANIFEST.MF里的參數(shù)
- 創(chuàng)建InstrumentationImpl對(duì)象
- 監(jiān)聽ClassFileLoadHook事件
- 調(diào)用InstrumentationImpl的loadClassAndCallAgentmain方法,在這個(gè)方法里會(huì)調(diào)用javaagent里MANIFEST.MF里指定的Agent-Class類的agentmain方法
instrument agent的ClassFileLoadHook回調(diào)實(shí)現(xiàn)
不管是啟動(dòng)時(shí)還是運(yùn)行時(shí)加載的instrument agent,都關(guān)注著同一個(gè)jvmti事件——ClassFileLoadHook,這個(gè)事件是在讀取字節(jié)碼文件之后回調(diào)時(shí)用的,這樣可以對(duì)原來(lái)的字節(jié)碼做修改,那這里面究竟是怎樣實(shí)現(xiàn)的呢?
void JNICALL
eventHandlerClassFileLoadHook( jvmtiEnv * jvmtienv,
JNIEnv * jnienv,
jclass class_being_redefined,
jobject loader,
const char* name,
jobject protectionDomain,
jint class_data_len,
const unsigned char* class_data,
jint* new_class_data_len,
unsigned char** new_class_data) {
JPLISEnvironment * environment = NULL;
environment = getJPLISEnvironment(jvmtienv);
/* if something is internally inconsistent (no agent), just silently return without touching the buffer */
if ( environment != NULL ) {
jthrowable outstandingException = preserveThrowable(jnienv);
transformClassFile( environment->mAgent,
jnienv,
loader,
name,
class_being_redefined,
protectionDomain,
class_data_len,
class_data,
new_class_data_len,
new_class_data,
environment->mIsRetransformer);
restoreThrowable(jnienv, outstandingException);
}
}
先根據(jù)jvmtiEnv取得對(duì)應(yīng)的JPLISEnvironment,因?yàn)樯厦嫖乙呀?jīng)說(shuō)到其實(shí)有兩個(gè)JPLISEnvironment(并且有兩個(gè)jvmtiEnv),其中一個(gè)是專門做retransform的,而另外一個(gè)用來(lái)做其他事情,根據(jù)不同的用途,在注冊(cè)具體的ClassFileTransformer時(shí)也是分開的,對(duì)于作為retransform用的ClassFileTransformer,我們會(huì)注冊(cè)到一個(gè)單獨(dú)的TransformerManager里。
接著調(diào)用transformClassFile方法,由于函數(shù)實(shí)現(xiàn)比較長(zhǎng),這里就不貼代碼了,大致意思就是調(diào)用InstrumentationImpl對(duì)象的transform方法,根據(jù)最后那個(gè)參數(shù)來(lái)決定選哪個(gè)TransformerManager里的ClassFileTransformer對(duì)象們做transform操作。
private byte[]
transform( ClassLoader loader,
String classname,
Class classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer,
boolean isRetransformer) {
TransformerManager mgr = isRetransformer?
mRetransfomableTransformerManager :
mTransformerManager;
if (mgr == null) {
return null; // no manager, no transform
} else {
return mgr.transform( loader,
classname,
classBeingRedefined,
protectionDomain,
classfileBuffer);
}
}
public byte[]
transform( ClassLoader loader,
String classname,
Class classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
boolean someoneTouchedTheBytecode = false;
TransformerInfo[] transformerList = getSnapshotTransformerList();
byte[] bufferToUse = classfileBuffer;
// order matters, gotta run 'em in the order they were added
for ( int x = 0; x < transformerList.length; x++ ) {
TransformerInfo transformerInfo = transformerList[x];
ClassFileTransformer transformer = transformerInfo.transformer();
byte[] transformedBytes = null;
try {
transformedBytes = transformer.transform( loader,
classname,
classBeingRedefined,
protectionDomain,
bufferToUse);
}
catch (Throwable t) {
// don't let any one transformer mess it up for the others.
// This is where we need to put some logging. What should go here? FIXME
}
if ( transformedBytes != null ) {
someoneTouchedTheBytecode = true;
bufferToUse = transformedBytes;
}
}
// if someone modified it, return the modified buffer.
// otherwise return null to mean "no transforms occurred"
byte [] result;
if ( someoneTouchedTheBytecode ) {
result = bufferToUse;
}
else {
result = null;
}
return result;
}
以上是最終調(diào)到的java代碼,可以看到已經(jīng)調(diào)用到我們自己編寫的javaagent代碼里了,我們一般是實(shí)現(xiàn)一個(gè)ClassFileTransformer類,然后創(chuàng)建一個(gè)對(duì)象注冊(cè)到對(duì)應(yīng)的TransformerManager里。
Class Transform的實(shí)現(xiàn)
這里說(shuō)的class transform其實(shí)是狹義的,主要是針對(duì)第一次類文件加載時(shí)就要求被transform的場(chǎng)景,在加載類文件的時(shí)候發(fā)出ClassFileLoad事件,然后交給instrumenat agent來(lái)調(diào)用javaagent里注冊(cè)的ClassFileTransformer實(shí)現(xiàn)字節(jié)碼的修改。
Class Redefine的實(shí)現(xiàn)
類重新定義,這是Instrumentation提供的基礎(chǔ)功能之一,主要用在已經(jīng)被加載過(guò)的類上,想對(duì)其進(jìn)行修改,要做這件事,我們必須要知道兩個(gè)東西,一個(gè)是要修改哪個(gè)類,另外一個(gè)是想將那個(gè)類修改成怎樣的結(jié)構(gòu),有了這兩個(gè)信息之后就可以通過(guò)InstrumentationImpl下面的redefineClasses方法操作了:
public void redefineClasses(ClassDefinition[] definitions) throws ClassNotFoundException {
if (!isRedefineClassesSupported()) {
throw new UnsupportedOperationException("redefineClasses is not supported in this environment");
}
if (definitions == null) {
throw new NullPointerException("null passed as 'definitions' in redefineClasses");
}
for (int i = 0; i < definitions.length; ++i) {
if (definitions[i] == null) {
throw new NullPointerException("element of 'definitions' is null in redefineClasses");
}
}
if (definitions.length == 0) {
return; // short-circuit if there are no changes requested
}
redefineClasses0(mNativeAgent, definitions);
}
在JVM里對(duì)應(yīng)的實(shí)現(xiàn)是創(chuàng)建一個(gè)VM_RedefineClasses的VM_Operation,注意執(zhí)行它的時(shí)候會(huì)stop-the-world:
jvmtiError
JvmtiEnv::RedefineClasses(jint class_count, const jvmtiClassDefinition* class_definitions) {
//TODO: add locking
VM_RedefineClasses op(class_count, class_definitions, jvmti_class_load_kind_redefine);
VMThread::execute(&op);
return (op.check_error());
} /* end RedefineClasses */
這個(gè)過(guò)程我盡量用語(yǔ)言來(lái)描述清楚,不詳細(xì)貼代碼了,因?yàn)榇a量實(shí)在有點(diǎn)大:
- 挨個(gè)遍歷要批量重定義的jvmtiClassDefinition
- 然后讀取新的字節(jié)碼,如果有關(guān)注ClassFileLoadHook事件的,還會(huì)走對(duì)應(yīng)的transform來(lái)對(duì)新的字節(jié)碼再做修改
- 字節(jié)碼解析好,創(chuàng)建一個(gè)klassOop對(duì)象
- 對(duì)比新老類,并要求如下:
- 父類是同一個(gè)
- 實(shí)現(xiàn)的接口數(shù)也要相同,并且是相同的接口
- 類訪問(wèn)符必須一致
- 字段數(shù)和字段名要一致
- 新增的方法必須是private static/final的
- 可以刪除修改方法
- 對(duì)新類做字節(jié)碼校驗(yàn)
- 合并新老類的常量池
- 如果老類上有斷點(diǎn),那都清除掉
- 對(duì)老類做JIT去優(yōu)化
- 對(duì)新老方法匹配的方法的jmethodId做更新,將老的jmethodId更新到新的method上
- 新類的常量池的holer指向老的類
- 將新類和老類的一些屬性做交換,比如常量池,methods,內(nèi)部類
- 初始化新的vtable和itable
- 交換annotation的method、field、paramenter
- 遍歷所有當(dāng)前類的子類,修改他們的vtable及itable
上面是基本的過(guò)程,總的來(lái)說(shuō)就是只更新了類里的內(nèi)容,相當(dāng)于只更新了指針指向的內(nèi)容,并沒有更新指針,避免了遍歷大量已有類對(duì)象對(duì)它們進(jìn)行更新所帶來(lái)的開銷。
Class Retransform的實(shí)現(xiàn)
retransform class可以簡(jiǎn)單理解為回滾操作,具體回滾到哪個(gè)版本,這個(gè)需要看情況而定,下面不管那種情況都有一個(gè)前提,那就是javaagent已經(jīng)要求要有retransform的能力了:
- 如果類是在第一次加載的的時(shí)候就做了transform,那么做retransform的時(shí)候會(huì)將代碼回滾到transform之后的代碼
- 如果類是在第一次加載的的時(shí)候沒有任何變化,那么做retransform的時(shí)候會(huì)將代碼回滾到最原始的類文件里的字節(jié)碼
- 如果類已經(jīng)加載了,期間類可能做過(guò)多次redefine(比如被另外一個(gè)agent做過(guò)),但是接下來(lái)加載一個(gè)新的agent要求有retransform的能力了,然后對(duì)類做redefine的動(dòng)作,那么retransform的時(shí)候會(huì)將代碼回滾到上一個(gè)agent最后一次做redefine后的字節(jié)碼
我們從InstrumentationImpl的retransformClasses方法參數(shù)看猜到應(yīng)該是做回滾操作,因?yàn)槲覀冎恢付薱lass:
public void retransformClasses(Class<?>[] classes) {
if (!isRetransformClassesSupported()) {
throw new UnsupportedOperationException( "retransformClasses is not supported in this environment");
}
retransformClasses0(mNativeAgent, classes);
}
不過(guò)retransform的實(shí)現(xiàn)其實(shí)也是通過(guò)redefine的功能來(lái)實(shí)現(xiàn),在類加載的時(shí)候有比較小的差別,主要體現(xiàn)在究竟會(huì)走哪些transform上,如果當(dāng)前是做retransform的話,那將忽略那些注冊(cè)到正常的TransformerManager里的ClassFileTransformer,而只會(huì)走專門為retransform而準(zhǔn)備的TransformerManager的ClassFileTransformer,不然想象一下字節(jié)碼又被無(wú)聲無(wú)息改成某個(gè)中間態(tài)了。
private:
void post_all_envs() {
if (_load_kind != jvmti_class_load_kind_retransform) {
// for class load and redefine,
// call the non-retransformable agents
JvmtiEnvIterator it;
for (JvmtiEnv* env = it.first(); env != NULL; env = it.next(env)) {
if (!env->is_retransformable() && env->is_enabled(JVMTI_EVENT_CLASS_FILE_LOAD_HOOK)) {
// non-retransformable agents cannot retransform back,
// so no need to cache the original class file bytes
post_to_env(env, false);
}
}
}
JvmtiEnvIterator it;
for (JvmtiEnv* env = it.first(); env != NULL; env = it.next(env)) {
// retransformable agents get all events
if (env->is_retransformable() && env->is_enabled(JVMTI_EVENT_CLASS_FILE_LOAD_HOOK)) {
// retransformable agents need to cache the original class file
// bytes if changes are made via the ClassFileLoadHook
post_to_env(env, true);
}
}
}
javaagent的其他小眾功能
javaagent除了做字節(jié)碼上面的修改之外,其實(shí)還有一些小功能,有時(shí)候還是挺有用的
- 獲取所有已經(jīng)被加載的類:Class[] getAllLoadedClasses();
- 獲取所有已經(jīng)初始化了的類: Class[] getInitiatedClasses(ClassLoader loader);
- 獲取某個(gè)對(duì)象的大?。?long getObjectSize(Object objectToSize);
- 將某個(gè)jar加入到bootstrap classpath里優(yōu)先其他jar被加載: void appendToBootstrapClassLoaderSearch(JarFile jarfile);
- 將某個(gè)jar加入到classpath里供appclassloard去加載:void appendToSystemClassLoaderSearch(JarFile jarfile);
- 設(shè)置某些native方法的前綴,主要在找native方法的時(shí)候做規(guī)則匹配: void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix)。
Debug的實(shí)現(xiàn)原理
IDEA 的 Debug 能查看斷點(diǎn)的上下文環(huán)境,更神奇的是我可以在斷點(diǎn)處使用它的 Evaluate 功能直接執(zhí)行某些命令,進(jìn)行一些計(jì)算或改變當(dāng)前變量。
很多時(shí)候,開發(fā)者都是“面向 Debug 開發(fā)”。

但 Java 是靜態(tài)語(yǔ)言,運(yùn)行之前是要先進(jìn)行編譯的,難道我寫的這些代碼是被實(shí)時(shí)編譯又”注入”到我正在 Debug 的服務(wù)里了嗎?
反射、字節(jié)碼、Btrace , Java 的 ASM 框架和 JVM TI 接口。
Java 代碼都是要被編譯成字節(jié)碼后才能放到 JVM 里執(zhí)行的,而字節(jié)碼一旦被加載到虛擬機(jī)中,就可以被解釋執(zhí)行。
字節(jié)碼文件(.class)就是普通的二進(jìn)制文件,它是通過(guò) Java 編譯器生成的。而只要是文件就可以被改變,如果我們用特定的規(guī)則解析了原有的字節(jié)碼文件,對(duì)它進(jìn)行修改或者干脆重新定義,這不就可以改變代碼行為了么。
Java 生態(tài)里有很多可以動(dòng)態(tài)生成字節(jié)碼的技術(shù),像 BCEL、Javassist、ASM、CGLib 等,它們各有自己的優(yōu)勢(shì)。有的使用復(fù)雜卻功能強(qiáng)大、有的簡(jiǎn)單確也性能些差。
ASM 框架
ASM 是它們中最強(qiáng)大的一個(gè),使用它可以動(dòng)態(tài)修改類、方法,甚至可以重新定義類,連 CGLib 底層都是用 ASM 實(shí)現(xiàn)的。
當(dāng)然,它的使用門檻也很高,使用它需要對(duì) Java 的字節(jié)碼文件有所了解,熟悉 JVM 的編譯指令。雖然我對(duì) JVM 的字節(jié)碼語(yǔ)法不熟,但有大神開發(fā)了可以在 IDEA 里查看字節(jié)碼的插件:ASM Bytecode Outline ,在要查看的類文件里右鍵選擇 Show bytecode Outline 即可以右側(cè)的工具欄查看我們要生成的字節(jié)碼。對(duì)照著示例,我們就可以很輕松地寫出操作字節(jié)碼的 Java 代碼了。
而切到 ASMified 標(biāo)簽欄,我們甚至可以直接獲取到 ASM 的使用代碼。
常用方法
在 ASM 的代碼實(shí)現(xiàn)里,最明顯的就是訪問(wèn)者模式,ASM 將對(duì)代碼的讀取和操作都包裝成一個(gè)訪問(wèn)者,在解析 JVM 加載到的字節(jié)碼時(shí)調(diào)用。
ClassReader 是 ASM 代碼的入口,通過(guò)它解析二進(jìn)制字節(jié)碼,實(shí)例化時(shí)它時(shí),我們需要傳入一個(gè) ClassVisitor,在這個(gè) Visitor 里,我們可以實(shí)現(xiàn) visitMethod()/visitAnnotation() 等方法,用以定義對(duì)類結(jié)構(gòu)(如方法、字段、注解)的訪問(wèn)方法。
而 ClassWriter 接口繼承了 ClassVisitor 接口,我們?cè)趯?shí)例化類訪問(wèn)器時(shí),將 ClassWriter “注入” 到里面,以實(shí)現(xiàn)對(duì)類寫入的聲明。
Instrument
介紹
字節(jié)碼是修改完了,可是 JVM 在執(zhí)行時(shí)會(huì)使用自己的類加載器加載字節(jié)碼文件,加載后并不會(huì)理會(huì)我們做出的修改,要想實(shí)現(xiàn)對(duì)現(xiàn)有類的修改,我們還需要搭配 Java 的另一個(gè)庫(kù) instrument。
instrument 是 JVM 提供的一個(gè)可以修改已加載類文件的類庫(kù)。1.6以前,instrument 只能在 JVM 剛啟動(dòng)開始加載類時(shí)生效,之后,instrument 更是支持了在運(yùn)行時(shí)對(duì)類定義的修改。
使用
要使用 instrument 的類修改功能,我們需要實(shí)現(xiàn)它的 ClassFileTransformer 接口定義一個(gè)類文件轉(zhuǎn)換器。它唯一的一個(gè) transform() 方法會(huì)在類文件被加載時(shí)調(diào)用,在 transform 方法里,我們可以對(duì)傳入的二進(jìn)制字節(jié)碼進(jìn)行改寫或替換,生成新的字節(jié)碼數(shù)組后返回,JVM 會(huì)使用 transform 方法返回的字節(jié)碼數(shù)據(jù)進(jìn)行類的加載。
JVM TI
定義完了字節(jié)碼的修改和重定義方法,但我們?cè)趺床拍茏?JVM 能夠調(diào)用我們提供的類轉(zhuǎn)換器呢?這里又要介紹到 JVM TI 了。
介紹
JVM TI(JVM Tool Interface)JVM 工具接口是 JVM 提供的一個(gè)非常強(qiáng)大的對(duì) JVM 操作的工具接口,通過(guò)這個(gè)接口,我們可以實(shí)現(xiàn)對(duì) JVM 多種組件的操作,從JVMTM Tool Interface 這里我們認(rèn)識(shí)到 JVM TI 的強(qiáng)大,它包括了對(duì)虛擬機(jī)堆內(nèi)存、類、線程等各個(gè)方面的管理接口。
JVM TI 通過(guò)事件機(jī)制,通過(guò)接口注冊(cè)各種事件勾子,在 JVM 事件觸發(fā)時(shí)同時(shí)觸發(fā)預(yù)定義的勾子,以實(shí)現(xiàn)對(duì)各個(gè) JVM 事件的感知和反應(yīng)。
Agent
Agent 是 JVM TI 實(shí)現(xiàn)的一種方式。我們?cè)诰幾g C 項(xiàng)目里鏈接靜態(tài)庫(kù),將靜態(tài)庫(kù)的功能注入到項(xiàng)目里,從而才可以在項(xiàng)目里引用庫(kù)里的函數(shù)。我們可以將 agent 類比為 C 里的靜態(tài)庫(kù),我們也可以用 C 或 C++ 來(lái)實(shí)現(xiàn),將其編譯為 dll 或 so 文件,在啟動(dòng) JVM 時(shí)啟動(dòng)。
這時(shí)再來(lái)思考 Debug 的實(shí)現(xiàn),我們?cè)趩?dòng)被 Debug 的 JVM 時(shí),必須添加參數(shù) -agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:3333,而 -agentlib 選項(xiàng)就指定了我們要加載的 Java Agent,jdwp 是 agent 的名字,在 linux 系統(tǒng)中,我們可以在 jre 目錄下找到 jdwp.so 庫(kù)文件。
Java 的調(diào)試體系 jdpa 組成,從高到低分別為 jdi->jdwp->jvmti,我們通過(guò) JDI 接口發(fā)送調(diào)試指令,而 jdwp 就相當(dāng)于一個(gè)通道,幫我們翻譯 JDI 指令到 JVM TI,最底層的 JVM TI 最終實(shí)現(xiàn)對(duì) JVM 的操作。
使用
JVM TI 的 agent 使用很簡(jiǎn)單,在啟動(dòng) agent 時(shí)添加 -agent 參數(shù)指定我們要加載的 agent jar包即可。
而要實(shí)現(xiàn)代碼的修改,我們需要實(shí)現(xiàn)一個(gè) instrument agent,它可以通過(guò)在一個(gè)類里添加 premain() 或 agentmain() 方法來(lái)實(shí)現(xiàn)。而要實(shí)現(xiàn) 1.6 以上的動(dòng)態(tài) instrument 功能,實(shí)現(xiàn) agentmain 方法即可。
在 agentmain 方法里,我們調(diào)用 Instrumentation.retransformClasses() 方法實(shí)現(xiàn)對(duì)目標(biāo)類的重定義。
另外往一個(gè)正在運(yùn)行的 JVM 里動(dòng)態(tài)添加 agent,還需要用到 JVM 的 attach 功能,Sun 公司的 tools.jar 包里包含的 VirtualMachine 類提供了 attach 一個(gè)本地 JVM 的功能,它需要我們傳入一個(gè)本地 JVM 的 pid, tools.jar 可以在 jre 目錄下找到。
agent生成
另外,我們還需要注意 agent 的打包,它需要指定一個(gè) Agent-Class 參數(shù)指定我們的包括 agentmain 方法的類,可以算是指定入口類吧。
此外,還需要配置 MANIFEST.MF 文件的一些參數(shù),允許我們重新定義類。如果你的 agent 實(shí)現(xiàn)還需要引用一些其他類庫(kù)時(shí),還需要將這些類庫(kù)都打包到此 jar 包中,下面是我的 pom 文件配置。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Agent-Class>asm.TestAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Manifest-Version>1.0</Manifest-Version>
<Permissions>all-permissions</Permissions>
</manifestEntries>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</build>
另外在打包時(shí)需要使用 mvn assembly:assembl 命令生成 jar-with-dependencies 作為 agent。
代碼實(shí)現(xiàn)
我在測(cè)試時(shí)寫了一個(gè)用以上技術(shù)實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的字節(jié)碼動(dòng)態(tài)修改的 Demo。
被修改的類
TransformTarget 是要被修改的目標(biāo)類,正常執(zhí)行時(shí),它會(huì)三秒輸出一次 “hello”。
public class TransformTarget {
public static void main(String[] args) {
while (true) {
try {
Thread.sleep(3000L);
} catch (Exception e) {
break;
}
printSomething();
}
}
public static void printSomething() {
System.out.println("hello");
}
}
Agent
Agent 是執(zhí)行修改類的主體,它使用 ASM 修改 TransformTarget 類的方法,并使用 instrument 包將修改提交給 JVM。
入口類,也是代理的 Agent-Class。
public class TestAgent {
public static void agentmain(String args, Instrumentation inst) {
inst.addTransformer(new TestTransformer(), true);
try {
inst.retransformClasses(TransformTarget.class);
System.out.println("Agent Load Done.");
} catch (Exception e) {
System.out.println("agent load failed!");
}
}
}
執(zhí)行字節(jié)碼修改和轉(zhuǎn)換的類。
public class TestTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("Transforming " + className);
ClassReader reader = new ClassReader(classfileBuffer);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
ClassVisitor classVisitor = new TestClassVisitor(Opcodes.ASM5, classWriter);
reader.accept(classVisitor, ClassReader.SKIP_DEBUG);
return classWriter.toByteArray();
}
class TestClassVisitor extends ClassVisitor implements Opcodes {
TestClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
if (name.equals("printSomething")) {
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(19, l0);
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("bytecode replaced!");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(20, l1);
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(2, 0);
mv.visitEnd();
TransformTarget.printSomething();
}
return mv;
}
}
}
Attacher
使用 tools.jar 里方法將 agent 動(dòng)態(tài)加載到目標(biāo) JVM 的類。
public class Attacher {
public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
VirtualMachine vm = VirtualMachine.attach("34242"); // 目標(biāo) JVM pid
vm.loadAgent("/path/to/agent.jar");
}
}
這樣,先啟動(dòng) TransformTarget 類,獲取到 pid 后將其傳入 Attacher 里,并指定 agent jar,將 agent attach 到 TransformTarget 中,原來(lái)輸出的 “hello” 就變成我們想要修改的 “bytecode replaced!” 了。
參考鏈接
https://www.cnblogs.com/zhenbianshu/p/10210597.html
https://docs.oracle.com/javase/7/docs/platform/jvmti/jvmti.html
https://www.cnblogs.com/beautiful-code/p/6424931.html
Kotlin開發(fā)者社區(qū)

專注分享 Java、 Kotlin、Spring/Spring Boot、MySQL、redis、neo4j、NoSQL、Android、JavaScript、React、Node、函數(shù)式編程、編程思想、"高可用,高性能,高實(shí)時(shí)"大型分布式系統(tǒng)架構(gòu)設(shè)計(jì)主題。