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 也发生内存溢出?
去哪儿国内酒店稳定性治理实践之缓存治理

相关文章
相关标签/搜索