入侵JVM? Java Agent原理浅析和实践

栏目: IT技术 · 发布时间: 3年前

内容简介:在平时的开发中,我们不可避免的会使用到Debug工具,JVM作为一个单独的进程,我们使用的Debug工具可以获取JVM运行时的相关的信息,查看变量值,甚至加入断点控制,还有我们平时使用JDK自带的JMAP、JSTACK等工具,可以在JVM运行时动态的dump内存、查询线程信息,甚至一些第三方的工具,比如说京东内部使用的JEX、pfinder,阿里巴巴的Arthas,优秀的开源的框架skywalking等等,也可以做到这些,那么这些工具究竟是通过什么技术手段来实现对JVM的监控和动态修改呢?本文会进行介绍和简

在平时的开发中,我们不可避免的会使用到Debug工具,JVM作为一个单独的进程,我们使用的Debug工具可以获取JVM运行时的相关的信息,查看变量值,甚至加入断点控制,还有我们平时使用JDK自带的JMAP、JSTACK等工具,可以在JVM运行时动态的dump内存、查询线程信息,甚至一些第三方的工具,比如说京东内部使用的JEX、pfinder,阿里巴巴的Arthas,优秀的开源的框架skywalking等等,也可以做到这些,那么这些 工具 究竟是通过什么技术手段来实现对JVM的监控和动态修改呢?本文会进行介绍和简单的原理分析,同时附带一些样例代码来进行分析。

从JVMTI说起

入侵JVM? Java Agent原理浅析和实践

JVM在设计之初,就考虑到了虚拟机状态的监控、debug、线程和内存分析等功能,在JDK5.0之前,JVM规范就定义了JVMPI(Java Virtual Machine Profiler Interface)也就是JVM分析接口以及JVMDI(Java Virtual Machine Debug Interface)也就是JVM调试接口,JDK5以及以后的版本,这两套接口合并成了一套,也就是Java Virtual Machine Tool Interface,就是我们这里说的JVMTI,这里需要注意的是:

  • JVMTI是一套JVM的接口规范,不同的JVM实现方式可以不同,有的JVM提供了拓展性的功能,比如openJ9,当然也可能存在JVM不提供这个接口的实现。

  • JVMTI提供的是Native方式调用的API,也就是常说的JNI方式,JVMTI接口用C/C++的语言提供,最终以动态链接库的形式由JVM加载并运行。

使用JNI方式调用JVMTI接口访问目标虚拟机的大体过程入下图:

入侵JVM? Java Agent原理浅析和实践

jvmti.h头文件中定义了JVMTI接口提供的方法,但是其方法的实现是由JVM提供商实现的,比如说hotspot虚拟机其实现大部分在src\share\vm\prims\jvmtiEnv.cpp这个文件中。

Instrument Agent

入侵JVM? Java Agent原理浅析和实践

在Jdk1.5之后,Java语言中开始提供Instrumentation接口(java.lang.instrument)让开发者可以使用 Java 语言编写Agent,但是其根本实现还是依靠JVMTI,只不过是SUN在工具包(sun.instrument.InstrumentationImpl)编写了一些native方法,并且然后在JDK里提供了这些native方法的实现类(jdk\src\share\instrument\JPLISAgent.c),最终需要调用jvmti.h头文件定义的方法,跟前文提到采用JNI方式访问JVMTI提供的方法并无差异,大体流程如下图:

入侵JVM? Java Agent原理浅析和实践

但是Instrument agent仅使用到了JVMTI提供部分功能,对开发者来说,主要提供的是对JVM加载的类字节码进行插桩操作。

入侵JVM? Java Agent原理浅析和实践

01

JVM启动时Agent

我们知道,JVM启动时可以指定-javaagent:xxx.jar参数来实现启动时代理,这里xxx.jar就是需要被代理到目标JVM上的JAR包,实现一个可以代理到指定JVM的JAR包需要满足以下条件:

  • JAR包的MANIFEST.MF清单文件中定义Premain-Class属性,指定一个类,加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项。

  • JAR包中包含清单文件中定义的这个类,类中包含premain方法,方法逻辑可以自己实现。

了解到这两点,我们可以定义下列类:

import java.lang.instrument.Instrumentation;



public class AgentMain {



// JVM启动时agent

public static void premain(String args, Instrumentation inst) {

agent0(args, inst);

}



public static void agent0(String args, Instrumentation inst) {

System.out.println("agent is running!");

// 添加一个类转换器

inst.addTransformer(new ClassFileTransformer() {

@Override

public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {

// JVM加载的所有类会流经这个类转换器

// 这里找到自定义的测试类

if (className.endsWith("WorkerMain")) {

System.out.println("transform class WorkerMain");

}

// 直接返回原本的字节码

return classfileBuffer;

}

});

}

}


JAR包内对应的清单文件(MANIFEST.MF)需要有如下内容:

PreMain-Class: AgentMain

Can-Redefine-Classes: true

Can-Retransform-Classes: true

-javaagent 所指定 jar 包内 Premain-Class 类的 premain 方法,方法签名可以有两种:

1. public static void premain(String agentArgs, Instrumentation inst)

2. public static void premain(String agentArgs)

JVM会优先加载1签名的方法,加载成功忽略2,如果1没有,加载2方法。这个逻辑在sun.instrument.InstrumentationImpl类中实现。

需要说明的是,addTransformer方法的作用是添加一个字节码转换器,这个方法的入参对象需要实现ClassFileTransformer接口,唯一需要实现的方法就是transform方法,这个方法可以用来修改加载类的字节码,目前我们并不对字节码进行修改。

最后定义测试类:package test;

import java.util.Random;



class WorkerMain {



public static void main(String[] args) throws InterruptedException {

for (; ; ) {

int x = new Random().nextInt();

new WorkerMain().test(x);

}

}



public void test(int x) throws InterruptedException {

Thread.sleep(2000);

System.out.println("i'm working " + x);

}

}


启动时添加-javaagent:xxx.jar参数,指定agent刚刚生成的JAR包,可以看到运行结果:

入侵JVM? Java Agent原理浅析和实践

下面尝试结合JDK源码对该流程进行浅析:

JVM开始启动时会解析-javaagent参数,如果存在这个参数,就会执行Agent_OnLoad 方法读取并解析指定JAR包后生成JPLISAgent对象,然后注册jvmtiEventCallbacks.VMInit这个事件,也就是虚拟机初始化事件,并设置该事件的回调函数eventHandlerVMInit,这些代码逻辑在jdk\src\share\instrument\InvocationAdapter.c 和 jdk\src\share\instrument\JPLISAgent.c 中实现。

入侵JVM? Java Agent原理浅析和实践

在JVM初始化时会调用之前注册的eventHandlerVMInit事件的回调函数,进入processJavaStart这个函数,首先会在注册另一个JVM事件ClassFileLoadHook,然后会真正的执行我们在Java代码层面编写的premain方法。当JVM开始装载类字节码文件时,会触发之前注册的ClassFileLoadHook事件的回调方法eventHandlerClassFileLoadHook,这个回调函数调用transformClassFile方法,生成新的字节码,被JVM装载,完成了启动时代理的全部流程。

以上代码逻辑在jdk\src\share\instrument\JPLISAgent.c 中实现。

入侵JVM? Java Agent原理浅析和实践

0 2

JVM运行时Agent

在JDK1.6版本中,SUN更进一步,提供了可以在JVM运行时代理的能力,和启动时代理类似,只需要满足:

  • JAR包的MANIFEST.MF清单文件中定义Agent-Class属性,指定一个类,加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项。

  • JAR包中包含清单文件中定义的这个类,类中包含agentmain方法,方法逻辑可以自己实现。

运行时Agent可以在JVM运行时动态的修改某个类的字节码,然后JVM会重定义这个类(不需要创建新的类加载器),但是为了保证JVM的正常运行,新定义的类相较于原来的类需要满足:

1. 父类是同一个。

2. 实现的接口数也要相同,并且是相同的接口。

3. 类访问符必须一致。

4. 字段数和字段名要一致。

5. 新增或删除的方法必须是private static/final的。

6. 可以修改方法内部代码。

运行时Agent需要借助JVM的Attach机制,简单来说就是JVM提供的一种通信机制,JVM中会存在一个Attach Listener线程,监听其他JVM的attach请求,其通信方式基于socket,JVM Attach机制大体流程图如下:

入侵JVM? Java Agent原理浅析和实践

JVM Attach

SUN在JDK中提供了Attach机制的Java语言工具包(com.sun.tools.attach),方便开发者使用Java语言进行操作,这里我们使用其中提供的loadAgent方法实现运行中agent的能力。

public class AttachUtil {



public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {



// 获取运行中的JVM列表

List<VirtualMachineDescriptor> vmList = VirtualMachine.list();

// 需要agent的jar包路径

String agentJar = "xxxx/agent-test.jar";

for (VirtualMachineDescriptor vmd : vmList) {

// 找到测试的JVM

if (vmd.displayName().endsWith("WorkerMain")) {

// attach到目标ID的JVM上

VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());

// agent指定jar包到已经attach的JVM上

virtualMachine.loadAgent(agentJar);

virtualMachine.detach();

}

}

}

同时对之前启动时Agent的代码进行改写:

public class AgentMain {




// JVM启动时agent

public static void premain(String args, Instrumentation inst) {

agent0(args, inst);

}



// JVM运行时agent

public static void agentmain(String args, Instrumentation inst) {

agent0(args, inst);

}



public static void agent0(String args, Instrumentation inst) {

System.out.println("agent is running!");

inst.addTransformer(new ClassFileTransformer() {

@Override

public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {

// 打印transform的类名

System.out.println(className);

return classfileBuffer;

}

},true);



try {

// 找到WorkerMain类,对其进行重定义

Class<?> c = Class.forName("test.WorkerMain");

inst.retransformClasses(c);

} catch (Exception e) {

System.out.println("error!");

}

}

}

这里我们也没有对字节码进行修改,还是直接返回原本的字节码。运行AttachUtil类,在目标JVM运行时完成了对其中test.WorkerMain 类的重新定义(虽然并没有修改字节码)。

入侵JVM? Java Agent原理浅析和实践

下面从JDK源码层面对整个流程进行浅析:

当AttachUtil的loadAgent方法调用时,目标JVM会调用自身的Agent_OnAttach方法,这个方法和之前提到的Agent_OnLoad 方法类似,会进行Agent JAR包的解析,不同的是Agent_OnAttach方法会直接注册ClassFileLoadHook事件回调函数,然后执行agentmain方法添加类转换器。

需要注意的是我们在Java代码里调用了Instrumentation#retransformClasses(Class<?>...)方法,追踪代码可以发现最终调用了一个native方法,而这个native方法的实现则在jdk的src\share\instrument\JPLISAgent.c类中,最终retransformClasses会调用到JVMTI的RetransformClasses方法,这里由于JVM源码实现非常复杂,感兴趣的同学可以自行阅读(hotspot源码路径src\share\vm\prims\jvmtiEnv.cpp),简单来说在这个方法里,JVM会触发ClassFileLoadHook事件回调完成类字节码的转换,并完成虚拟机内已经加载的类字节码的热替换。

入侵JVM? Java Agent原理浅析和实践

至此,在JVM运行时悄无声息的完成了类的重定义,不得不佩服JDK开发者的高超手段。

运行方法分析

入侵JVM? Java Agent原理浅析和实践

了解到上述机制以后,我们可以通过在目标JVM运行时对其中的类进行重新定义,做到运行时插桩代码。

我们知道ASM是一个字节码修改框架,因此就可以在类转换器中,对原本类的字节码进行修改,然后再对这个类进行重定义(retransform)。

首先我们实现ClassFileTransformer接口,前文中在transform方法中并没有对于字节码进行修改,只是单纯的打印了一些信息,既然需要对字目标类的节码进行修改,我们需要了解下ClassFileTransformer接口中唯一需要实现的方法transform,方法签名如下:

byte[]

transform( ClassLoader loader,

String className,

Class<?> classBeingRedefined,

ProtectionDomain protectionDomain,

byte[] classfileBuffer)

throws IllegalClassFormatException;

可以看到方法入参有该类的类加载器、类名、类Class对象、类的保护域、以及最重要的classfileBuffer,也就是这个类的字节码,此时就可以借助ASM这个字节码大杀器来为所欲为了。现在我们实现一个字节的类转换器MyClassTransformer,然后使用ASM来对字节码进行修改。

public class MyClassTransformer implements ClassFileTransformer {



@Override

public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {



// 对类字节码进行操作

// 这里需要注意,不能对classfileBuffer这个数组进行修改操作

try {

// 创建ASM ClassReader对象,导入需要增强的对象字节码

ClassReader reader = new ClassReader(classfileBuffer);

ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);

// 自己实现的代码增强器

MyEnhancer myEnhancer = new MyEnhancer(classWriter);



// 增强字节码

reader.accept(myEnhancer, ClassReader.SKIP_FRAMES);



// 返回MyEnhancer增强后的字节码

return classWriter.toByteArray();

} catch (Exception e) {

e.printStackTrace();

}



// return null 则不会对类进行转换

return null;

}

}


至此,我们拼上了JVM运行时插桩代码的最后一块拼图,这样就可以理解Arthas这类基于Java Agent的性能分析工具是如何在JVM运行时对你的代码进行了修改。

入侵JVM? Java Agent原理浅析和实践

接着实现一个字节码增强器,借助ASM将对方法入参和方法耗时的监控代码织入,这里需要对字节码有一定了解,这里笔者使用到ASM提供的AdviceAdapter类简化开发。

public class MyEnhancer extends ClassVisitor implements Opcodes {



public MyEnhancer(ClassVisitor classVisitor) {

super(ASM7, classVisitor);

}



/**

* 对字节码中的方法定义进行修改

*/

@Override

public MethodVisitor visitMethod(int access, final String name, String descriptor, String signature, String[] exceptions) {

MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);

if (isIgnore(mv, access, name)) {

return mv;

}

return new AdviceAdapter(Opcodes.ASM7, new JSRInlinerAdapter(mv, access, name, descriptor, signature, exceptions), access, name, descriptor) {



private final Type METHOD_CONTAINER = Type.getType(MethodContainer.class);

private int timeIdentifier;

private int argsIdentifier;



/**

* 进入方法前

*/

@Override

protected void onMethodEnter() {

// 调用System.nanoTime()方法,将方法出参推入栈顶

invokeStatic(Type.getType(System.class), Method.getMethod("long nanoTime()"));

// 构造一个Long类型的局部变量,然后返回这个变量的标识符

timeIdentifier = newLocal(Type.LONG_TYPE);



// 存储栈顶元素也就是System.nanoTime()返回值,到指定位置本地变量区

storeLocal(timeIdentifier);



// 加载入参数组,将入参数组ref推入栈顶

loadArgArray();

// 构造一个Object[]类型的局部变量,返回这个变量的标识符

argsIdentifier = newLocal(Type.getType(Object[].class));

// 存储入参到指定位置本地变量区

storeLocal(argsIdentifier);

}



@Override

protected void onMethodExit(int opcode) {

// 加载指定位置的本地变量到栈顶

loadLocal(timeIdentifier);

loadLocal(argsIdentifier);

// 相当于调用MethodContainer.showMethod(long, Object[])方法

invokeStatic(METHOD_CONTAINER, Method.getMethod("void showMethod(long,Object[])"));

}



};

}



/**

* 方法是否需要被忽略(静态构造函数和构造函数)

*/

private boolean isIgnore(MethodVisitor mv, int access, String methodName) {

return null == mv

|| isAbstract(access)

|| isFinalMethod(access)

|| "<clinit>".equals(methodName)

|| "<init>".equals(methodName);

}



private boolean isAbstract(int access) {

return (ACC_ABSTRACT & access) == ACC_ABSTRACT;

}



private boolean isFinalMethod(int methodAccess) {

return (ACC_FINAL & methodAccess) == ACC_FINAL;

}



}

由于这里对于字节码的修改是在方法内部,那么实现一些复杂逻辑的最好方式,就是调用外部类的静态方法,虚拟机字节码指令中的invokestatic 是调用指定类的静态方法的指令,这里我们将方法开始时间和方法入参作为参数调用MethodContainer.showMethod 方法,方法实现如下:

public class MethodContainer {



// 实现静态方法

public static void showMethod(long startTime, Object[] Args) {

System.out.println("方法耗时:" + (System.nanoTime() - startTime) / 1000000 + "ms, 方法入参:" + Arrays.toString(Args));

}



}



ASM操作字节码需要一定的学习才能理解,如果把上述字节码增强前后用Java代码表示大体入下:

// ASM代码增强前

public void test(int x) throws InterruptedException {

Thread.sleep(2000L);

System.out.println("i'm working " + x);

}



// ASM代码增强后

public void test(int x) throws InterruptedException {

long var2 = System.nanoTime();

Object[] var4 = new Object[]{new Integer(x)};

Thread.sleep(2000L);

System.out.println("i'm working " + x);

MethodContainer.showMethod(var2, var4);

}

最后运行AttachUitl,可以看到正在运行中的JVM被成功的插入了我们实现的字节码,对于目标虚拟机来说是完全不需要任何实现的,而且被重定义的代码也可以被还原,感兴趣的同学可以自己了解下。

入侵JVM? Java Agent原理浅析和实践

总结

入侵JVM? Java Agent原理浅析和实践

对于Java开发者来说,代码插桩是很熟悉的一个概念,而且目前也有很多成熟的方式可以完成,比如说Spring AOP实现采用的动态代理方式,Lombok采用的插入式注解处理器方式等。

入侵JVM? Java Agent原理浅析和实践

所谓术业有专攻,Instrument Agent虽然强大,但也不见得适用所有的场景,对于日志统计、方法监控,动态代理已经能很好的满足这方面的需求,但是对于JVM性能监控或方法实时运行分析,Instrument Agent可以随时插入、随时卸载、随时修改的特性就体现出了极大的优点,同时其基于Java代码开发又会相应的降低一些开发难度,这也是业内很多性能分析软件选择这种方式实现的原因。

入侵JVM? Java Agent原理浅析和实践

说明

本文中JDK源码部分基于Open JDK 8u60版本 进行分析

本文中使用到的ASM版本为8.0.1

入侵JVM? Java Agent原理浅析和实践

参考文章

Oracle Docs - JVMTI:

https://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html

你假笨 - JVM源码分析之javaagent原理完全解读:

http://lovestblog.cn/blog/2015/09/14/javaagent/


以上所述就是小编给大家介绍的《入侵JVM? Java Agent原理浅析和实践》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Algorithms and Theory of Computation Handbook

Algorithms and Theory of Computation Handbook

Mikhail J. Atallah (Editor) / CRC-Press / 1998-09-30 / USD 94.95

Book Description This comprehensive compendium of algorithms and data structures covers many theoretical issues from a practical perspective. Chapters include information on finite precision issues......一起来看看 《Algorithms and Theory of Computation Handbook》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

随机密码生成器
随机密码生成器

多种字符组合密码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具