原文地址:http://www.infoq.com/cn/articles/javaagent-illustrated

概述

本文重点讲述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文件之前做拦截,对字节码做修改
  • 可以在运行期对已加载类的字节码做变更,但是这种情况下会有很多的限制,后面会详细说
  • 还有其他一些小众的功能
    • 获取所有已经加载过的类
    • 获取所有已经初始化过的类(执行过clinit方法,是上面的一个子集)
    • 获取某个对象的大小
    • 将某个jar加入到bootstrap classpath里作为高优先级被bootstrapClassloader加载
    • 将某个jar加入到classpath里供AppClassloard去加载
    • 设置某些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 void JNICALL
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初始化完成之后做下面的事情:
    • 创建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操作。

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

以上是最终调到的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加入到bootstrap classpath里优先其他jar被加载: void appendToBootstrapClassLoaderSearch(JarFile jarfile);
  • 将某个jar加入到classpath里供appclassloard去加载:void appendToSystemClassLoaderSearch(JarFile jarfile);
  • 设置某些native方法的前缀,主要在找native方法的时候做规则匹配: void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix)。

JVM源码分析之javaagent原理完全解读--转的更多相关文章

  1. JVM 源码分析之 javaagent 原理完全解读

    转载:https://infoq.cn/article/javaagent-illustrated 本文重点讲述 javaagent 的具体实现,因为它面向的是我们 Java 程序员,而且 agent ...

  2. JVM源码分析之javaagent原理完全解读

    概述 本文重点讲述javaagent的具体实现,因为它面向的是我们Java程序员,而且agent都是用Java编写的,不需要太多的C/C++编程基础,不过这篇文章里也会讲到JVMTIAgent(C实现 ...

  3. Guava 源码分析(Cache 原理 对象引用、事件回调)

    前言 在上文「Guava 源码分析(Cache 原理)」中分析了 Guava Cache 的相关原理. 文末提到了回收机制.移除时间通知等内容,许多朋友也挺感兴趣,这次就这两个内容再来分析分析. 在开 ...

  4. JVM源码分析之SystemGC完全解读

    JVM源码分析之SystemGC完全解读 概述 JVM的GC一般情况下是JVM本身根据一定的条件触发的,不过我们还是可以做一些人为的触发,比如通过jvmti做强制GC,通过System.gc触发,还可 ...

  5. 深入源码分析SpringMVC底层原理(二)

    原文链接:深入源码分析SpringMVC底层原理(二) 文章目录 深入分析SpringMVC请求处理过程 1. DispatcherServlet处理请求 1.1 寻找Handler 1.2 没有找到 ...

  6. JVM源码分析之一个Java进程究竟能创建多少线程

    JVM源码分析之一个Java进程究竟能创建多少线程 原创: 寒泉子 你假笨 2016-12-06 概述 虽然这篇文章的标题打着JVM源码分析的旗号,不过本文不仅仅从JVM源码角度来分析,更多的来自于L ...

  7. 【转】MaBatis学习---源码分析MyBatis缓存原理

    [原文]https://www.toutiao.com/i6594029178964673027/ 源码分析MyBatis缓存原理 1.简介 在 Web 应用中,缓存是必不可少的组件.通常我们都会用 ...

  8. JVM源码分析之堆外内存完全解读

    JVM源码分析之堆外内存完全解读   寒泉子 2016-01-15 17:26:16 浏览6837 评论0 阿里技术协会 摘要: 概述 广义的堆外内存 说到堆外内存,那大家肯定想到堆内内存,这也是我们 ...

  9. JVM源码分析之Metaspace解密

        概述 metaspace,顾名思义,元数据空间,专门用来存元数据的,它是jdk8里特有的数据结构用来替代perm,这块空间很有自己的特点,前段时间公司这块的问题太多了,主要是因为升级了中间件所 ...

随机推荐

  1. 08.十分钟学会JSP传统标签编程

    一.认识标签 1,说明:传统标签编程在开发中基本用不到,学习标签编程主要还是为了完善知识体系. 2,标签的主要作用:移除或减少jsp中的java代码 3,标签的主要组成部分及运行原理 4,简单标签示例 ...

  2. 树的问题小结(最小生成树、次小生成树、最小树形图、LCA、最小支配集、最小点覆盖、最大独立集)

    树的定义:连通无回路的无向图是一棵树. 有关树的问题: 1.最小生成树. 2.次小生成树. 3.有向图的最小树形图. 4.LCA(树上两点的最近公共祖先). 5.树的最小支配集.最小点覆盖.最大独立集 ...

  3. powershell遍历文件夹设置权限,解决文件无法删除的问题。

    function set-rights ($path) { $p = Get-Item $path; if ($p.Attributes -eq 'Directory') { foreach ($ch ...

  4. Service、chkconfig命令

    转自:http://my.oschina.net/phptiger86/blog/137656

  5. Java之关于面向对象

    面向对象,呃,别给我说程序员找不到对象,那是windows才会出现的情况~~~ 就简单记下笔记什么的吧. 1.关于定义和赋值 之前总是搞混淆,说到底是没有搞清楚. shit bigOne=new sh ...

  6. Resolving Strong Reference Cycles for Closures

    You resolve a strong reference cycle between a closure and a class instance by defining a capture li ...

  7. js中时间戳与日期时间之间的相互转换

    1.时间戳转换为标准日期时间格式: function timeFormat(dateStr) { var date = new Date(dateStr); Y = date.getFullYear( ...

  8. django rest-farme-work 的使用(3)

    请求和响应 Requests and Responses 从这一片来说,我们将真正开始覆盖REST框架的核心.我们来介绍一些基本的构建块 Request objects REST框架引入了一个Requ ...

  9. 基于Vue的事件响应式进度条组件

    写在前面 找了很多Vue 进度条组件!,都不包含拖拽和点击事件,input range倒是原生包含input和change事件,但是直接基于input range做进度条的话,样式部分需要做大量调整和 ...

  10. centos solr 集群搭建

    一.什么是 SolrCloud SolrCloud(solr 云)是 Solr 提供的分布式搜索方案,当你需要大规模,容错,分布式索引和检索能力时使用 SolrCloud.当一个系统的索引数据量少的时 ...