JVM源碼分析之javaagent原理徹底解讀

概述

本文重點講述javaagent的具體實現,由於它面向的是咱們java程序員,並且agent都是用java編寫的,不須要太多的c/c++編程基礎,不過這篇文章裏也會講到JVMTIAgent(c實現的),由於javaagent的運行仍是依賴於一個特殊的JVMTIAgent。java

對於javaagent或許你們都聽過,甚至使用過,常見的用法大體以下:linux

java -javaagent:myagent.jar=mode=test Test

咱們經過-javaagent來指定咱們編寫的agent的jar路徑(./myagent.jar)及要傳給agent的參數(mode=test),這樣在啓動的時候這個agent就能夠作一些咱們想要它作的事了。c++

javaagent的主要的功能以下:程序員

  • 能夠在加載class文件以前作攔截把字節碼作修改
  • 能夠在運行期將已經加載的類的字節碼作變動,可是這種狀況下會有不少的限制,後面會詳細說
  • 還有其餘的一些小衆的功能編程

    • 獲取全部已經被加載過的類
    • 獲取全部已經被初始化過了的類(執行過了clinit方法,是上面的一個子集)
    • 獲取某個對象的大小
    • 將某個jar加入到bootstrapclasspath裏做爲高優先級被bootstrapClassloader加載
    • 將某個jar加入到classpath裏供AppClassloard去加載
    • 設置某些native方法的前綴,主要在查找native方法的時候作規則匹配
    • 想象一下可讓程序按照咱們預期的邏輯去執行,聽起來是否是挺酷的。

JVMTI

JVMTI全稱JVM Tool Interface,是jvm暴露出來的一些供用戶擴展的接口集合,JVMTI是基於事件驅動的,JVM每執行到必定的邏輯就會調用一些事件的回調接口(若是有的話),這些接口能夠供開發者去擴展本身的邏輯。bootstrap

好比說咱們最多見的想在某個類的字節碼文件讀取以後類定義以前能修改相關的字節碼,從而使建立的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參數。app

javaagent

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

instrument agent

instrument agent實現了Agent_OnLoadAgent_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.cAgent_OnLoad方法裏,簡單描述下過程:

  • 建立並初始化JPLISAgent
  • 監聽VMInit事件,在vm初始化完成以後作下面的事情:

    • 建立InstrumentationImpl對象
    • 監聽ClassFileLoadHook事件
    • 調用InstrumentationImpl的loadClassAndCallPremain方法,在這個方法裏會去調用javaagent裏MANIFEST.MF裏指定的Premain-Class類的premain方法
  • 解析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操做。

image.png

以上是最終調到的java代碼,能夠看到已經調用到咱們本身編寫的javaagent代碼裏了,咱們通常是實現一個ClassFileTransformer類,而後建立一個對象註冊了對應的TransformerManager裏。

Class Transform的實現

這裏說的class transform實際上是狹義的,主要是針對第一次類文件加載的時候就要求被transform的場景,在加載類文件的時候發出ClassFileLoad的事件,而後交給instrumenat 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

上面是基本的過程,總的來講就是隻更新了類裏內容,至關於只更新了指針指向的內容,並無更新指針,避免了遍歷大量已有類對象對它們進行更新帶來的開銷。

Class Retransform的實現

retransform class能夠簡單理解爲回滾操做,具體回滾到哪一個版本,這個須要看狀況而定,下面無論那種狀況都有一個前提,那就是javaagent已經要求要有retransform的能力了:

若是類是在第一次加載的的時候就作了transform,那麼作retransform的時候會將代碼回滾到transform以後的代碼
若是類是在第一次加載的的時候沒有任何變化,那麼作retransform的時候會將代碼回滾到最原始的類文件裏的字節碼
若是類已經被加載了,期間類可能作過屢次redefine(好比被另一個agent作過),可是接下來加載一個新的agent要求有retransform的能力了,而後對類作redefine的動做,那麼retransform的時候會將代碼回滾到上一個agent最後一次作redefine後的字節碼
咱們從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的其餘小衆功能

javaagent除了作字節碼上面的修改以外,其實還有一些小功能,有時候仍是挺有用的

  • 獲取全部已經被加載的類
Class[] getAllLoadedClasses();
  • 獲取全部已經被初始化過了的類
Class[] getInitiatedClasses(ClassLoader loader);
  • 獲取某個對象的大小
long getObjectSize(Object objectToSize);
  • 將某個jar加入到bootstrapclasspath裏優先其餘jar被加載
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
  • 將某個jar加入到classpath裏供appclassloard去加載
void appendToSystemClassLoaderSearch(JarFile jarfile);
  • 設置某些native方法的前綴,主要在找native方法的時候作規則匹配
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);

推薦閱讀
強如 Disruptor 也發生內存溢出?
去哪兒國內酒店穩定性治理實踐之緩存治理

相關文章
相關標籤/搜索