1、JVMTI 介绍

JVMTI(JVM Tool Interface)是 Java 虚拟机所提供的 native 编程接口,是 JVMPI(Java Virtual Machine Profiler Interface)和 JVMDI(Java Virtual Machine Debug Interface)的替代版本。
JVMTI可以用来开发并监控虚拟机,可以查看JVM内部的状态,并控制JVM应用程序的执行。可实现的功能包括但不限于:调试、监控、线程分析、覆盖率分析工具等。
另外,需要注意的是,并非所有的JVM实现都支持JVMTI。
JVMTI只是一套接口,我们要开发JVM工具就需要写一个Agent程序来使用这些接口。Agent程序其实就是一个C/C++语言编写的动态链接库。这里不详细介绍如何开发一个JVMTI的agent程序。感兴趣的可以点击文章末尾的链接查看。
我们通过JVMTI开发好agent程序后,把程序编译成动态链接库,之后可以在jvm启动时指定加载运行该agent。

  1. -agentlib:<agent-lib-name>=<options>

之后JVM启动后该agent程序就会开始工作

1.1 Agent的工作形式

agent启动后是和JVM运行在同一个进程,大多agent的工作形式是作为服务端接收来自客户端的请求,然后根据请求命令调用JVMTI的相关接口再返回结果。
很多java监控、诊断工具都是基于这种形式来工作的。如果arthas、jinfo、brace等。
另外,我们熟知的java调试也是其实也是基于这种工作原理。

1.2 JDPA 相关介绍

无论我们在开发调试时,都会用到调试工具。其实我们用的所有调试工具其底层都是基于JVMTI的调用。JVMTI本身就提供了关于调试程序的一系列接口,我们只需要编写agent就可以开发一套调试工具了。
虽然对应的接口已经有了,但是要基于这些接口开发一套完整的调试工具还是有一定工作量的。为了避免重复造轮子,sun公司定义了一套完整独立的调试体系,也就是JDPA。
JDPA由3个模块组成:

  1. JVMTI,即底层的相关调试接口调用。sun公司提供了一个 jdwp.dll( jdwp.so)动态链接库,就是我们上面说的agent实现。
  2. JDWP(Java Debug Wire Protocol),定义了agent和调试客户端之间的通讯交互协议。
  3. JDI(Java Debug Interface),是由Java语言实现的。有了这套接口,我们就可以直接使用java开发一套自己的调试工具。

Java JVMTI 和 Instrumention 机制介绍 - 图1
其实有了jdwp Agent以及知道了交互的消息协议格式,我们就可以基于这些开发一套调试工具了。但是相对还是比较费时费力,所以才有了JDI的诞生,JDI是一套JAVA API。这样对于不熟悉C/C++的java程序员也能开发自己的调试工具了。

另外,JDI 不仅能帮助开发人员格式化 JDWP 数据,而且还能为 JDWP 数据传输提供队列、缓存等优化服务

再回头看一下启动JVM debug时需要带上的参数:

  1. java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000 -jar test.jar

jdwp.dll 作为一个 jvm 内置的 agent,不需要上文说的 -agentlib 来启动agent。这里通过-Xrunjdwp 来启动该 agent。后面还指定了一些参数:

  • transport=dt_socket,表示用监听socket端口的方式来建立连接,这里也可以选择dt_shmem共享内存方式,但限于windows机器,并且服务端和客户端位于一台机器上
  • server=y 表示当前是调试服务端,=n表示当前是调试客户端
  • suspend=n 表示启动时不中断(如果启动时中断,一般用于调试启动不了的问题)
  • address=8000 表示本地监听8000端口

    2、Instrumention 机制

    虽然java提供了JVMTI,但是对应的agent需要用C/C++开发,对java开发者而言并不是非常友好。因此在Java SE 5的新特性中加入了Instrumentation机制。
    有了 Instrumentation,开发者可以构建一个基于Java编写的Agent来监控或者操作JVM了,比如替换或者修改某些类的定义等。

    2.1 Instrumention支持的功能

    Instrumention支持的功能都在java.lang.instrument.Instrumentation接口中体现:

    1. public interface Instrumentation {
    2. //添加一个ClassFileTransformer
    3. //之后类加载时都会经过这个ClassFileTransformer转换
    4. void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
    5. void addTransformer(ClassFileTransformer transformer);
    6. //移除ClassFileTransformer
    7. boolean removeTransformer(ClassFileTransformer transformer);
    8. boolean isRetransformClassesSupported();
    9. //将一些已经加载过的类重新拿出来经过注册好的ClassFileTransformer转换
    10. //retransformation可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
    11. void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
    12. boolean isRedefineClassesSupported();
    13. //重新定义某个类
    14. void redefineClasses(ClassDefinition... definitions)
    15. throws ClassNotFoundException, UnmodifiableClassException;
    16. boolean isModifiableClass(Class<?> theClass);
    17. @SuppressWarnings("rawtypes")
    18. Class[] getAllLoadedClasses();
    19. @SuppressWarnings("rawtypes")
    20. Class[] getInitiatedClasses(ClassLoader loader);
    21. long getObjectSize(Object objectToSize);
    22. void appendToBootstrapClassLoaderSearch(JarFile jarfile);
    23. void appendToSystemClassLoaderSearch(JarFile jarfile);
    24. boolean isNativeMethodPrefixSupported();
    25. void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
    26. }

    我们通过addTransformer方法注册了一个ClassFileTransformer,后面类加载的时候都会经过这个Transformer处理。对于已加载过的类,可以调用retransformClasses来重新触发这个Transformer的转换
    ClassFileTransformer可以判断是否需要修改类定义并根据自己的代码规则修改类定义然后返回给JVM。利用这个Transformer类,我们可以很好的实现虚拟机层面的AOP。

    redefineClasses 和 retransformClasses 的区别:

    1. transform是对类的byte流进行读取转换的过程,需要先获取类的byte流然后做修改。而redefineClasses更简单粗暴一些,它需要直接给出新的类byte流,然后替换旧的
    2. transform可以添加很多个,retransformClasses 可以让指定的类重新经过这些transform做转换。

2.2 基于Instrumention开发一个Agent

利用java.lang.instrument包下面的相关类,我们可以开发一个自己的Agent程序。

2.2.1 编写premain函数

编写一个java类,不用继承或者实现任何类,直接实现下面两个方法中的任一方法:

  1. //agentArgs是一个字符串,会随着jvm启动设置的参数得到
  2. //inst就是我们需要的Instrumention实例了,由JVM传入。我们可以拿到这个实例后进行各种操作
  3. public static void premain(String agentArgs, Instrumentation inst); [1]
  4. public static void premain(String agentArgs); [2]

其中,[1] 的优先级比 [2] 高,将会被优先执行,[1] 和 [2] 同时存在时,[2] 被忽略。
编写一个PreMain:

  1. public class PreMain {
  2. public static void premain(String agentArgs, Instrumentation inst) throws ClassNotFoundException,
  3. UnmodifiableClassException {
  4. inst.addTransformer(new MyTransform());
  5. }
  6. }

MyTransform是我们自己定义的一个ClassFileTransformer实现类,这个类遇到com/yjb/Test类,就会进行类定义转换。

  1. public class MyTransform implements ClassFileTransformer {
  2. public static final String classNumberReturns2 = "/tmp/Test.class";
  3. public static byte[] getBytesFromFile(String fileName) {
  4. try {
  5. // precondition
  6. File file = new File(fileName);
  7. InputStream is = new FileInputStream(file);
  8. long length = file.length();
  9. byte[] bytes = new byte[(int) length];
  10. // Read in the bytes
  11. int offset = 0;
  12. int numRead = 0;
  13. while (offset < bytes.length
  14. && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
  15. offset += numRead;
  16. }
  17. if (offset < bytes.length) {
  18. throw new IOException("Could not completely read file "
  19. + file.getName());
  20. }
  21. is.close();
  22. return bytes;
  23. } catch (Exception e) {
  24. System.out.println("error occurs in _ClassTransformer!"
  25. + e.getClass().getName());
  26. return null;
  27. }
  28. }
  29. /**
  30. * 参数:
  31. * loader - 定义要转换的类加载器;如果是引导加载器,则为 null
  32. * className - 完全限定类内部形式的类名称和 The Java Virtual Machine Specification 中定义的接口名称。例如,"java/util/List"。
  33. * classBeingRedefined - 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
  34. * protectionDomain - 要定义或重定义的类的保护域
  35. * classfileBuffer - 类文件格式的输入字节缓冲区(不得修改)
  36. * 返回:
  37. * 一个格式良好的类文件缓冲区(转换的结果),如果未执行转换,则返回 null。
  38. * 抛出:
  39. * IllegalClassFormatException - 如果输入不表示一个格式良好的类文件
  40. */
  41. public byte[] transform(ClassLoader l, String className, Class<?> c,
  42. ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {
  43. System.out.println("transform class-------" + className);
  44. if (!className.equals("com/yjb/Test")) {
  45. return null;
  46. }
  47. return getBytesFromFile(targetClassPath);
  48. }
  49. }

2.2.2 打成jar包

之后我们把上面两个类打成一个jar包,并在其中的META-INF/MAINIFEST.MF属性当中加入” Premain-Class”来指定成上面的PreMain类。
我们可以用maven插件来做到自动打包并写MAINIFEST.MF:

  1. <plugin>
  2. <groupId>org.apache.maven.plugins</groupId>
  3. <artifactId>maven-assembly-plugin</artifactId>
  4. <executions>
  5. <execution>
  6. <goals>
  7. <goal>single</goal>
  8. </goals>
  9. <phase>package</phase>
  10. <configuration>
  11. <descriptorRefs>
  12. <descriptorRef>jar-with-dependencies</descriptorRef>
  13. </descriptorRefs>
  14. <archive>
  15. <manifestEntries>
  16. <Premain-Class>com.yjb.PreMain</Premain-Class>
  17. <Can-Redefine-Classes>true</Can-Redefine-Classes>
  18. <Can-Retransform-Classes>true</Can-Retransform-Classes>
  19. <Specification-Title>${project.name}</Specification-Title>
  20. <Specification-Version>${project.version}</Specification-Version>
  21. <Implementation-Title>${project.name}</Implementation-Title>
  22. <Implementation-Version>${project.version}</Implementation-Version>
  23. </manifestEntries>
  24. </archive>
  25. </configuration>
  26. </execution>
  27. </executions>
  28. </plugin>

2.2.3 编写测试类

上面的agent会转换com/yjb/Test类,我们就编写一个Test类进行测试。

  1. public class Test {
  2. public void print() {
  3. System.out.println("A");
  4. }
  5. }

先编译这个类,然后把Test.class 放到 /tmp 下。
之后再修改这个类:

  1. public class Test {
  2. public void print() {
  3. System.out.println("B");
  4. }
  5. public static void main(String[] args) throws InterruptedException {
  6. new Test().print();
  7. }
  8. }

之后运行时指定加上JVM参数 -javaagent:/toPath/agent-jar-with-dependencies.jar 就会发现Test已经被转换了

2.3 如何在运行时加载agent

上面开发的agent需要启动就必须在jvm启动时设置参数,但很多时候我们想要在程序运行时中途插入一个agent运行。在Java 6的新特性中,就可以通过Attach的方式去加载一个agent了。
关于Attach的机制原理可以看我的这篇博客:
https://blog.csdn.net/u013332124/article/details/88362317
使用这种方式加载的agent启动类需要实现这两种方法中的一种:

  1. public static void agentmain (String agentArgs, Instrumentation inst); [1]
  2. public static void agentmain (String agentArgs);[2]

和premain一样,[1] 比 [2] 的优先级高。
之后要在META-INF/MAINIFEST.MF属性当中加入” AgentMain-Class”来指定目标启动类
我们可以在上面的agent项目中加入一个AgentMain类

  1. public class AgentMain {
  2. public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException,
  3. UnmodifiableClassException, InterruptedException {
  4. //这里的Transform还是使用上面定义的那个
  5. inst.addTransformer(new MyTransform(), true);
  6. //由于是在运行中才加入了Transform,因此需要重新retransformClasses一下
  7. Class<?> aClass = Class.forName("com.yjb.Test");
  8. inst.retransformClasses(aClass);
  9. System.out.println("Agent Main Done");
  10. }
  11. }

还是把项目打包成agent-jar-with-dependencies.jar
之后再编写一个类去attach目标进程并加载这个agent

  1. public class AgentMainStarter {
  2. public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException,
  3. AgentInitializationException {
  4. //这个pid填写具体要attach的目标进程
  5. VirtualMachine attach = VirtualMachine.attach("pid");
  6. attach.loadAgent("/toPath/agent-jar-with-dependencies.jar");
  7. attach.detach();
  8. System.out.println("over");
  9. }
  10. }

之后修改一下Test类,让他不断运行下去

  1. public class Test {
  2. private void print() {
  3. System.out.println("1111");
  4. }
  5. public static void main(String[] args) throws InterruptedException {
  6. Test test = new Test();
  7. while (true) {
  8. test.print();
  9. Thread.sleep(1000L);
  10. }
  11. }
  12. }

运行Test一段时间后,再运行AgentMainStarter类,会发现输出变成了最早编译的那个/tmp/Test.class下面的”A”了。说明我们的agent进程已经在目标JVM成功运行。

3、参考资料

Java Attach机制简介
基于Java Instrument的Agent实现
IBM: Instrumentation 新功能
Instrumentation 中redefineClasses 和 retransformClasses 的区别
JVMTI开发文档
JVMTI oracle 官方文档
JVMTI和JDPA介绍