JVM源码分析之javaagent原理完全解读(转载)

概述

本文重点讲述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的主要功能如下:

·      可以在加载class文件之前做拦截,对字节码做修改

·      可以在运行期对已加载类的字节码做变更,但是这种情况下会有很多的限制,后面会详细说

·      还有其他一些小众的功能

o  获取所有已经加载过的类

o  获取所有已经初始化过的类(执行过clinit方法,是上面的一个子集)

o  获取某个对象的大小

o  将某个jar加入到bootstrap classpath里作为高优先级被bootstrapClassloader加载

o  将某个jar加入到classpath里供AppClassloard去加载

o  设置某些native方法的前缀,主要在查找native方法的时候做规则匹配

想象一下可以让程序按照我们预期的逻辑去执行,听起来是不是挺酷的。

JVMTI

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 voidJNICALL

Agent_OnUnload(JavaVM *vm);

·      Agent_OnLoad函数,如果agent是在启动时加载的,也就是在vm参数里通过-agentlib来指定的,那在启动过程中就会去执行这个agent里的Agent_OnLoad函数。

·      Agent_OnAttach函数,如果agent不是在启动时加载的,而是我们先attach到目标进程上,然后给对应的目标进程发送load命令来加载,则在加载过程中会调用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既可以在启动时加载,也可以在运行时动态加载。其中启动时加载还可以通过类似-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 */

};

这里解释一下几个重要项:

·      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`方法里,这里简单描述下过程:

·      创建并初始化JPLISAgent

·      监听VMInit事件,在vm初始化完成之后做下面的事情:

o  创建InstrumentationImpl对象

o  监听ClassFileLoadHook事件

o  调用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,这个事件是在读取字节码文件之后回调时用的,这样可以对原来的字节码做修改,那这里面究竟是怎样实现的呢?

voidJNICALL

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) {

      booleansomeoneTouchedTheBytecode = 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的实现

这里说的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_RedefineClassesop(class_count, class_definitions,jvmti_class_load_kind_redefine);

VMThread::execute(&op);

  return (op.check_error());

} /* end RedefineClasses */

这个过程我尽量用语言来描述清楚,不详细贴代码了,因为代码量实在有点大:

·      挨个遍历要批量重定义的jvmtiClassDefinition

·      然后读取新的字节码,如果有关注ClassFileLoadHook事件的,还会走对应的transform来对新的字节码再做修改

·      字节码解析好,创建一个klassOop对象

·      对比新老类,并要求如下:

o  父类是同一个

o  实现的接口数也要相同,并且是相同的接口

o  类访问符必须一致

o  字段数和字段名要一致

o  新增的方法必须是private static/final的

o  可以删除修改方法

·      对新类做字节码校验

·      合并新老类的常量池

·      如果老类上有断点,那都清除掉

·      对老类做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加入到bootstrap classpath里优先其他jar被加载: void

appendToBootstrapClassLoaderSearch(JarFile jarfile);

·      将某个jar加入到classpath里供appclassloard去加载:void appendToSystemClassLoaderSearch(JarFile

jarfile);

·      设置某些native方法的前缀,主要在找native方法的时候做规则匹配: void

setNativeMethodPrefix(ClassFileTransformer transformer, String prefix)。

作者简介

李嘉鹏,花名寒泉子,使用“你假笨”的ID混迹网络

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容