日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

JVM插桩之二:Java agent基础原理

發布時間:2024/1/23 java 16 豆豆
生活随笔 收集整理的這篇文章主要介紹了 JVM插桩之二:Java agent基础原理 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Javaagent只要作用在class被加載之前對其加載,插入我們需要添加的字節碼。

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使用說明

javaagent主要作用

可以在加載java文件之前做攔截把字節碼做修改

可以在運行期將已經加載的類的字節碼做變更,但是這種情況下會有很多的限制,后面會詳細說
還有其他的一些小眾的功能

獲取所有已經被加載過的類

獲取所有已經被初始化過了的類(執行過了clinit方法,是上面的一個子集)

獲取某個對象的大小
將某個jar加入到bootstrapclasspath里作為高優先級被bootstrapClassloader加載

將某個jar加入到classpath里供AppClassload去加載

設置某些native方法的前綴,主要在查找native方法的時候做規則匹配

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

JVMTIAgent其實就是一個動態庫,利用JVMTI暴露出來的一些接口來干一些我們想做但是正常情況下又做不到的事情,不過為了和普通的動態庫進行區分,它一般會實現如下的一個或者多個函數:

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函數,如果agent是在啟動的時候加載的,也就是在vm參數里通過-agentlib來指定,那在啟動過程中就會去執行這個agent里的Agent_OnLoad函數。

Agent_OnAttach函數,如果agent不是在啟動的時候加載的,是我們先attach到目標進程上,然后給對應的目標進程發送load命令來加載agent,在加載過程中就會調用Agent_OnAttach函數。

Agent_OnUnload函數,在agent做卸載的時候調用,不過貌似基本上很少實現它。

其實我們每天都在和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

說到javaagent必須要講的是一個叫做instrument的JVMTIAgent(linux下對應的動態庫是libinstrument.so),因為就是它來實現javaagent的功能的,另外instrument agent還有個別名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),從這名字里也完全體現了其最本質的功能:就是專門為java語言編寫的插樁服務提供支持的。

INSTRUMENT?AGENT

instrument agent實現了Agent_OnLoad和Agent_OnAttach兩方法,也就是說我們在用它的時候既支持啟動的時候來加載agent,也支持在運行期來動態來加載這個agent,其中啟動時加載agent還可以通過類似-javaagent:myagent.jar的方式來間接加載instrument agent,運行期動態加載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 */ };

這里解釋下幾個重要項:

  • mNormalEnvironment:主要提供正常的類transform及redefine功能的。
  • mRetransformEnvironment:主要提供類retransform功能的。
  • mInstrumentationImpl:這個對象非常重要,也是我們java agent和JVM進行交互的入口,或許寫過javaagent的人在寫premain以及agentmain方法的時候注意到了有個Instrumentation的參數,這個參數其實就是這里的對象。
  • mPremainCaller:指向sun.instrument.InstrumentationImpl.loadClassAndCallPremain方法,如果agent是在啟動的時候加載的,那該方法會被調用。
  • mAgentmainCaller:指向sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain方法,該方法在通過attach的方式動態加載agent的時候調用。
  • mTransform:指向sun.instrument.InstrumentationImpl.transform方法。
  • mAgentClassName:在我們javaagent的MANIFEST.MF里指定的Agent-Class。
  • mOptionsString:傳給agent的一些參數。
  • mRedefineAvailable:是否開啟了redefine功能,在javaagent的MANIFEST.MF里設置Can-Redefine-Classes:true。
  • mNativeMethodPrefixAvailable:是否支持native方法前綴設置,通樣在javaagent的MANIFEST.MF里設置Can-Set-Native-Method-Prefix:true。
  • mIsRetransformer:如果在javaagent的MANIFEST.MF文件里定義了Can-Retransform-Classes:true,那將會設置mRetransformEnvironment的mIsRetransformer為true。

紅色標注的是我們最常用的,下面的列子也是會用到的...,接下來看一個具體的例子,如果熟悉分布式調用鏈系統的人肯定知道,調用鏈中最基礎的一個功能就是統計一個服務里面的某個方法執行了多長時間...,其實這個就目前來說大多數系統底層都是基于字節碼插樁技術實現的,接下來就演示一個完整的例子....,定義一個業務類,類里面定義幾個方法,然后在執行這個方法的時候,會動態實現方法的耗時統計。示例見《》。

啟動時加載instrument agent

正如『概述』里提到的方式,就是啟動的時候加載instrument agent,具體過程都在InvocationAdapter.c的Agent_OnLoad方法里,簡單描述下過程:

1.創建并初始化JPLISAgent;

2.監聽VMInit事件,在vm初始化完成之后坐下面的事情;

3.創建InstrumentationImpl對象;

4.監聽ClassFileLoadHook事件;

5.調用InstrumentationImpl的loadClassAndCallPremain方法,在這個方法里會去調用javaagent里MANIFEST.MF里指定的Premain-Class類的premain方法;

6.解析javaagent里MANIFEST.MF里的參數,并根據這些參數來設置JPLISAgent里的一些內容;

運行時加載instrument agent

運行時加載的方式,大致按照下面的方式來操作:

VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent(agentPath, agentArgs);

上面會通過jvm的attach機制來請求目標jvm加載對應的agent,過程大致如下:

  • 創建并初始化JPLISAgent
  • 解析javaagent里MANIFEST.MF里的參數
  • 創建InstrumentationImpl對象
  • 監聽ClassFileLoadHook事件
  • 調用InstrumentationImpl的loadClassAndCallAgentmain方法,在這個方法里會去調用javaagent里MANIFEST.MF里指定的Agent-Class類的agentmain方法

instrument agent的ClassFileLoadHook回調實現

不管是啟動時還是運行時加載的instrument agent都關注著同一個jvmti事件---ClassFileLoadHook,這個事件是在讀取字節碼文件之后回調時用的,這樣可以對原來的字節碼做修改,那這里面究竟是怎樣實現的呢?

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);} }

先根據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 addedfor ( 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的實現

這里說的class transform其實是狹義的,主要是針對第一次類文件加載的時候就要求被transform的場景,在加載類文件的時候發出ClassFileLoad的事件,然后交給instrument agent來調用javaagent里注冊的ClassFileTransformer實現字節碼的修改。

Class Redefine的實現

類重新定義,這是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的:

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 */

這個過程我盡量用語言來描述清楚,不詳細貼代碼了,因為代碼量實在有點大:

  • 挨個遍歷要批量重定義的jvmtiClassDefinition
  • 然后讀取新的字節碼,如果有關注ClassFileLoadHook事件的,還會走對應的transform來對新的字節碼再做修改
  • 字節碼解析好,創建一個klassOop對象
  • 對比新老類,并要求如下:
  • 父類是同一個
  • 實現的接口數也要相同,并且是相同的接口
  • 類訪問符必須一致
  • 字段數和字段名要一致
  • 新增或刪除的方法必須是private static/final的
  • 可以修改方法
    • 對新類做字節碼校驗
    • 合并新老類的常量池
    • 如果老類上有斷點,那都清除掉
    • 對老類做jit去優化
    • 對新老方法匹配的方法的jmethodid做更新,將老的jmethodId更新到新的method上
    • 新類的常量池的holer指向老的類
    • 將新類和老類的一些屬性做交換,比如常量池,methods,內部類
    • 初始化新的vtable和itable
    • 交換annotation的method,field,paramenter
    • 遍歷所有當前類的子類,修改他們的vtable及itable

    上面是基本的過程,總的來說就是只更新了類里內容,相當于只更新了指針指向的內容,并沒有更新指針,避免了遍歷大量已有類對象對它們進行更新帶來的開銷。

    更多的信息見原貼:《https://blog.csdn.net/ancinsdn/article/details/58276945》

    ?

    總結

    以上是生活随笔為你收集整理的JVM插桩之二:Java agent基础原理的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。