JVMTM Tool Interface:JVM源碼分析之javaagent原理完全解讀

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.cAgent_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ì)主題。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容