JVM中的动态追踪技术

x33g5p2x  于2021-12-18 转载在 其他  
字(4.7k)|赞(0)|评价(0)|浏览(556)

动态追踪技术

动态追踪技术是一个可以不用重启线上java项目来进行问题排查的技术,也叫Java Agent技术,可以利用它来构建一个附加的代理程序,用来协助检测性能,还可以替换一些现有功能,甚至JDK的一些类我们也能修改,有点像JVM级别的AOP功能。

既然作为JVM的AOP,就必须要有AOP的功能,所以Java Agent提供了两个类似于AOP的方法:

  • premain():在main()方法运行之前的进行一些操作。
  • agentmain():可以控制类运行时的行为。
    但在一个JVM中,只会调用一个。

已有一个项目案例agent-demo:

  1. package com.morris.demo;
  2. public class MainRun {
  3. public static void main(String[] args) throws InterruptedException {
  4. while (true) {
  5. new Account().hello();
  6. Thread.sleep(1000);
  7. }
  8. }
  9. }
  10. package com.morris.demo;
  11. public class Account {
  12. public void hello() {
  13. System.out.println("hello world");
  14. }
  15. }

要构建一个agent程序,大体可分为以下步骤:

  1. 使用字节码增强工具,编写增强代码。
  2. 在MANIFEST.MF中指定Premain-Class/Agent-Class属性,并打包成jar。
  3. 使用参数加载或者使用attach方式改变agent-demo项目中的内容。

编写Agent

Java Agent体现方式是一个jar包,这里为了简单起见,将agent和项目放在同一个工程中。

加入maven依赖,我们借用javassist完成字节码增强:

  1. <dependency>
  2. <groupId>org.javassist</groupId>
  3. <artifactId>javassist</artifactId>
  4. <version>3.24.1-GA</version>
  5. </dependency>

创建一个普通的Java类,添加premain或者agentmain方法,它们的参数完全一样,示例中使用的是AgentApp。

  1. package com.morris.agent;
  2. import com.morris.demo.Account;
  3. import java.lang.instrument.Instrumentation;
  4. import java.lang.instrument.UnmodifiableClassException;
  5. public class AgentApp {
  6. //在main执行之前的修改
  7. public static void premain(String agentOps, Instrumentation inst) {
  8. System.out.println("==============enter premain==============");
  9. inst.addTransformer(new Agent());
  10. }
  11. //控制类运行时的行为
  12. public static void agentmain(String agentOps, Instrumentation inst) throws UnmodifiableClassException {
  13. System.out.println("==============enter agentmain==============");
  14. inst.addTransformer(new Agent());
  15. inst.retransformClasses(Account.class);
  16. }
  17. }

编写Transformer

假如我们要统计某个方法的执行时间,使用JavaAssist工具来增强字节码。

例如要增强demo项目中MainRun类的hello方法,那么编写一个Agent类实现ClassFileTransformer接口,然后在transform方法中实现以下逻辑:

  1. 获取MainRun类的字节码。
  2. 获取hello()方法的字节码。
  3. 在hello()方法前后,加入时间统计的代码。
  4. 最后把字节码返回。
  1. package com.morris.agent;
  2. import javassist.ClassPool;
  3. import javassist.CtClass;
  4. import javassist.CtMethod;
  5. import java.lang.instrument.ClassFileTransformer;
  6. import java.lang.instrument.IllegalClassFormatException;
  7. import java.security.ProtectionDomain;
  8. public class Agent implements ClassFileTransformer {
  9. @Override
  10. public byte[] transform(ClassLoader loader, String className,
  11. Class<?> classBeingRedefined,
  12. ProtectionDomain protectionDomain,
  13. byte[] classfileBuffer) throws IllegalClassFormatException {
  14. if (className.endsWith("Account")) {
  15. try {
  16. String loadName = className.replaceAll("/", ".");
  17. CtClass ctClass = ClassPool.getDefault().get(loadName);
  18. CtMethod ctMethod = ctClass.getDeclaredMethod("hello");
  19. ctMethod.addLocalVariable("_begin", CtClass.longType);
  20. ctMethod.insertBefore("_begin = System.nanoTime();");
  21. ctMethod.insertAfter("System.out.println(System.nanoTime() - _begin);");
  22. return ctClass.toBytecode();
  23. } catch (Throwable e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. return classfileBuffer;
  28. }
  29. }

打包Agent

在src/main/resources/META-INF/目录下新建MANIFEST.MF文件,内容如下:

  1. Manifest-Version: 1.0
  2. Can-Redefine-Classes: true
  3. Can-Retransform-Classes: true
  4. Premain-class: com.morris.agent.AgentApp
  5. Agent-class: com.morris.agent.AgentApp

由于maven打包过程中会自己创建MANIFEST.MF,所有需要指定maven使用我们MANIFEST.MF文件:

  1. <plugin>
  2. <groupId>org.apache.maven.plugins</groupId>
  3. <artifactId>maven-jar-plugin</artifactId>
  4. <configuration>
  5. <archive>
  6. <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
  7. </archive>
  8. </configuration>
  9. </plugin>

最后使用mvn package命令进行打包,得到agent-1.0-SNAPSHOT.jar包。

使用

Premain

直接命令行中加入-javaagent参数即可:

  1. java -javaagent:C:\development\project\agent-demo\target\agent-demo-1.0-SNAPSHOT.jar -classpath C:\development\project\agent-demo\demo\target\classes;C:\Users\user\.m2\repository\org\javassist\javassist\3.24.1-GA\javassist-3.24.1-GA.jar com.morris.demo.MainRun

运行结果如下:

  1. ==============enter premain==============
  2. hello world
  3. 157632

执行后,直接输出hello world。通过增强以后,还额外的输出了执行时间,以及一些debug信息。其中,debug信息在main方法执行之前输出。

Agentmain

Agentmain这种模式一般用在一些诊断工具上,使用jdk/lib/tools.jar中的工具类中的Attach API,可以动态的为运行中的程序加入一些功能。它的主要操作步骤如下:

  1. 获取机器上运行的所有JVM进程ID,类似jps命令。
  2. 选择要诊断的jvm。
  3. 将jvm使用attach函数链接上。
  4. 使用loadAgent函数加载agent,动态修改字节码。
  5. 卸载jvm。
  1. package com.morris.demo;
  2. import com.sun.tools.attach.VirtualMachine;
  3. import com.sun.tools.attach.VirtualMachineDescriptor;
  4. import java.util.List;
  5. import java.util.Properties;
  6. public class JvmAttach {
  7. public static void main(String[] args)
  8. throws Exception {
  9. List<VirtualMachineDescriptor> list = VirtualMachine.list();
  10. VirtualMachine virtualMachine = null;
  11. for (VirtualMachineDescriptor vmd : list) {
  12. if (vmd.displayName().endsWith("MainRun")) {
  13. virtualMachine = VirtualMachine.attach(vmd.id());
  14. virtualMachine.loadAgent("C:\\development\\project\\agent-demo\\target\\agent-demo-1.0-SNAPSHOT.jar", "agent");
  15. break;
  16. }
  17. }
  18. System.in.read();
  19. virtualMachine.detach();
  20. }
  21. }

注意:写这段代码的时候IDE可能提示找不到jar包,这时候将jdk/lib/tools.jar添加的项目的classpath中。

相关文章