本文重點講述javaagent的具體實現,因為它面向的是我們Java程序員,而且agent都是用Java編寫的,不需要太多的C/C++編程基礎,不過這篇文章里也會講到JVMTIAgent(C實現的),因為javaagent的運行還是依賴于一個特殊的JVMTIAgent。
對于javaagent,或許大家都聽過,甚至使用過,常見的用法大致如下:
java -javaagent:myagent.jar=mode=test Test
我們通過-javaagent來指定我們編寫的agent的jar路徑(./myagent.jar),以及要傳給agent的參數(mode=test),在啟動的時候這個agent就可以做一些我們希望的事了。
javaagent的主要功能如下:
想象一下可以讓程序按照我們預期的邏輯去執行,聽起來是不是挺酷的。
JVMTI全稱JVM Tool Interface,是JVM暴露出來的一些供用戶擴展的接口集合。JVMTI是基于事件驅動的,JVM每執行到一定的邏輯就會調用一些事件的回調接口(如果有的話),這些接口可以供開發者擴展自己的邏輯。
比如最常見的,我們想在某個類的字節碼文件讀取之后、類定義之前修改相關的字節碼,從而使創建的class對象是我們修改之后的字節碼內容,那就可以實現一個回調函數賦給jvmtiEnv(JVMTI的運行時,通常一個JVMTIAgent對應一個jvmtiEnv,但是也可以對應多個)的回調方法集合里的ClassFileLoadHook,這樣在接下來的類文件加載過程中都會調用到這個函數中,大致實現如下:,
jvmtiEventCallbacks callbacks; jvmtiEnv * jvmtienv = jvmti(agent); jvmtiError jvmtierror; memset(&callbacks, 0, sizeof(callbacks)); callbacks.ClassFileLoadHook = &eventHandlerClassFileLoadHook; jvmtierror = (*jvmtienv)->SetEventCallbacks( jvmtienv, &callbacks, sizeof(callbacks));
JVMTIAgent其實就是一個動態庫,利用JVMTI暴露出來的一些接口來干一些我們想做、但是正常情況下又做不到的事情,不過為了和普通的動態庫進行區分,它一般會實現如下的一個或者多個函數:
JNIEXPORT jint JNICALLAgent_OnLoad(JavaVM *vm, char *options, void *reserved);JNIEXPORT jint JNICALLAgent_OnAttach(JavaVM* vm, char* options, void* reserved);JNIEXPORT void JNICALLAgent_OnUnload(JavaVM *vm);
其實我們每天都在和JVMTIAgent打交道,只是你可能沒有意識到而已,比如我們經常使用Eclipse等工具調試Java代碼,其實就是利用JRE自帶的jdwp agent實現的,只是Eclipse等工具在沒讓你察覺的情況下將相關參數(類似-agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:61349)自動加到程序啟動參數列表里了,其中agentlib參數就用來跟要加載的agent的名字,比如這里的jdwp(不過這不是動態庫的名字,JVM會做一些名稱上的擴展,比如在linux下會去找libjdwp.so的動態庫進行加載,也就是在名字的基礎上加前綴lib,再加后綴.so),接下來會跟一堆相關的參數,將這些參數傳給Agent_OnLoad或者Agent_OnAttach函數里對應的options。
說到javaagent,必須要講的是一個叫做instrument的JVMTIAgent(Linux下對應的動態庫是libinstrument.so),因為javaagent功能就是它來實現的,另外instrument agent還有個別名叫JPLISAgent(Java PRogramming Language Instrumentation Services Agent),這個名字也完全體現了其最本質的功能:就是專門為Java語言編寫的插樁服務提供支持的。
instrument agent實現了Agent_OnLoad和Agent_OnAttach兩方法,也就是說在使用時,agent既可以在啟動時加載,也可以在運行時動態加載。其中啟動時加載還可以通過類似-javaagent:myagent.jar的方式來間接加載instrument agent,運行時動態加載依賴的是JVM的attach機制(JVM Attach機制實現),通過發送load命令來加載agent。
instrument agent的核心數據結構如下:
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 */};
這里解釋一下幾個重要項:
正如前面“概述”里提到的方式,就是啟動時加載instrument agent,具體過程都在`InvocationAdapter.c`的`Agent_OnLoad`方法里,這里簡單描述下過程:
在運行時加載的方式,大致按照下面的方式來操作:
VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent(agentPath, agentArgs);
上面會通過JVM的attach機制來請求目標JVM加載對應的agent,過程大致如下:
不管是啟動時還是運行時加載的instrument agent,都關注著同一個jvmti事件——ClassFileLoadHook,這個事件是在讀取字節碼文件之后回調時用的,這樣可以對原來的字節碼做修改,那這里面究竟是怎樣實現的呢?
void JNICALLeventHandlerClassFileLoadHook( 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); }}
先根據jvmtiEnv取得對應的JPLISEnvironment,因為上面我已經說到其實有兩個JPLISEnvironment(并且有兩個jvmtiEnv),其中一個是專門做retransform的,而另外一個用來做其他事情,根據不同的用途,在注冊具體的ClassFileTransformer時也是分開的,對于作為retransform用的ClassFileTransformer,我們會注冊到一個單獨的TransformerManager里。
接著調用transformClassFile方法,由于函數實現比較長,這里就不貼代碼了,大致意思就是調用InstrumentationImpl對象的transform方法,根據最后那個參數來決定選哪個TransformerManager里的ClassFileTransformer對象們做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; }
以上是最終調到的java代碼,可以看到已經調用到我們自己編寫的javaagent代碼里了,我們一般是實現一個ClassFileTransformer類,然后創建一個對象注冊到對應的TransformerManager里。
這里說的class transform其實是狹義的,主要是針對第一次類文件加載時就要求被transform的場景,在加載類文件的時候發出ClassFileLoad事件,然后交給instrumenat agent來調用javaagent里注冊的ClassFileTransformer實現字節碼的修改。
類重新定義,這是Instrumentation提供的基礎功能之一,主要用在已經被加載過的類上,想對其進行修改,要做這件事,我們必須要知道兩個東西,一個是要修改哪個類,另外一個是想將那個類修改成怎樣的結構,有了這兩個信息之后就可以通過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里對應的實現是創建一個VM_RedefineClasses的VM_Operation,注意執行它的時候會stop-the-world:
jvmtiErrorJvmtiEnv::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 */
這個過程我盡量用語言來描述清楚,不詳細貼代碼了,因為代碼量實在有點大:
上面是基本的過程,總的來說就是只更新了類里的內容,相當于只更新了指針指向的內容,并沒有更新指針,避免了遍歷大量已有類對象對它們進行更新所帶來的開銷。
retransform class可以簡單理解為回滾操作,具體回滾到哪個版本,這個需要看情況而定,下面不管那種情況都有一個前提,那就是javaagent已經要求要有retransform的能力了:
我們從InstrumentationImpl的retransformClasses方法參數看猜到應該是做回滾操作,因為我們只指定了class:
public void retransformClasses(Class<?>[] classes) { if (!isRetransformClassesSupported()) { throw new UnsupportedOperationException( "retransformClasses is not supported in this environment"); } retransformClasses0(mNativeAgent, classes); }
不過retransform的實現其實也是通過redefine的功能來實現,在類加載的時候有比較小的差別,主要體現在究竟會走哪些transform上,如果當前是做retransform的話,那將忽略那些注冊到正常的TransformerManager里的ClassFileTransformer,而只會走專門為retransform而準備的TransformerManager的ClassFileTransformer,不然想象一下字節碼又被無聲無息改成某個中間態了。
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除了做字節碼上面的修改之外,其實還有一些小功能,有時候還是挺有用的
新聞熱點
疑難解答