JVM中的动态追踪技术

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

动态追踪技术

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

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

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

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

package com.morris.demo;

public class MainRun {
    public static void main(String[] args) throws InterruptedException {
        while (true) {
            new Account().hello();
            Thread.sleep(1000);
        }
    }
}

package com.morris.demo;

public class Account {
    public void hello() {
        System.out.println("hello world");
    }
}

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

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

编写Agent

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

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

<dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.24.1-GA</version>
        </dependency>

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

package com.morris.agent;

import com.morris.demo.Account;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class AgentApp {
    //在main执行之前的修改
    public static void premain(String agentOps, Instrumentation inst) {
        System.out.println("==============enter premain==============");
        inst.addTransformer(new Agent());
    }
    //控制类运行时的行为
    public static void agentmain(String agentOps, Instrumentation inst) throws UnmodifiableClassException {
        System.out.println("==============enter agentmain==============");
        inst.addTransformer(new Agent());
        inst.retransformClasses(Account.class);
    }
}

编写Transformer

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

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

  1. 获取MainRun类的字节码。
  2. 获取hello()方法的字节码。
  3. 在hello()方法前后,加入时间统计的代码。
  4. 最后把字节码返回。
package com.morris.agent;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class Agent implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className,
                            Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) throws IllegalClassFormatException {
        if (className.endsWith("Account")) {
            try {
                String loadName = className.replaceAll("/", ".");
                CtClass ctClass = ClassPool.getDefault().get(loadName);
                CtMethod ctMethod = ctClass.getDeclaredMethod("hello");
                ctMethod.addLocalVariable("_begin", CtClass.longType);
                ctMethod.insertBefore("_begin = System.nanoTime();");
                ctMethod.insertAfter("System.out.println(System.nanoTime() - _begin);");

                return ctClass.toBytecode();
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
        return classfileBuffer;
    }
}

打包Agent

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

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-class: com.morris.agent.AgentApp
Agent-class: com.morris.agent.AgentApp

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

<plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
                    </archive>
                </configuration>
            </plugin>

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

使用

Premain

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

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

运行结果如下:

==============enter premain==============
hello world
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。
package com.morris.demo;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;
import java.util.Properties;

public class JvmAttach {

    public static void main(String[] args)
            throws Exception {
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        VirtualMachine virtualMachine = null;
        for (VirtualMachineDescriptor vmd : list) {
            if (vmd.displayName().endsWith("MainRun")) {
                virtualMachine = VirtualMachine.attach(vmd.id());
                virtualMachine.loadAgent("C:\\development\\project\\agent-demo\\target\\agent-demo-1.0-SNAPSHOT.jar", "agent");
                break;
            }
        }

        System.in.read();
        virtualMachine.detach();
    }
}

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

相关文章