动态追踪之java agent
上篇文章我们说到阿里的诊断工具Arthas对方法和类的监控使用的是动态追踪技术,本文我们将介绍动态追踪技术Java Agent。
Java Agent是什么?
Java Agent技术,也被称为Java代理、Java探针,从JDK1.5它就出现了,它允许程序员利⽤其构建⼀个独⽴于应⽤程序的代理程序。Java Agent本身就是个jar包,它利用JVM提供的Instrumentation API来更改加载在JVM中的现有字节码,Java Agent可以理解为是JVM级别的AOP。
Java Agent 怎么用?
Java Agent使用包括两个部分:代码实现和配置文件。
- 代码实现:这部分包括3个步骤:
- 实现ClassFileTransformer接口并重写transform方法。 ClassFileTransformer用于在JVM加载实现类之前转换类文件。
- 编写agent类
agent类中有两个方法:
premain
方法和agentmain
方法。premain
用于在代理方法执行前调用,在 JVM 启动时必须为其指明Java代理。它有两个实现agentmain
用于在代理方法运行时执行。
- 修改配置文件并打包
- 配置文件:配置文件名为MANIFEST.MF,需放在META-INF文件夹下或者在maven中配置。
Premain-Class:表示实现premain方法的类。
Agent-Class:表示实现agentmain方法的类。
Can-Redefine-Classes:表示Java agent是否可以重新定义被代理类。
Can-Retransform-Classes:表示是否允许Java Agent转换被代理类。
Java Agent的加载
Java Agent的加载分为静态加载和动态加载。
静态加载
在应用程序启动时加载Java代理称为静态加载,静态加载在任何代码执行之前在启动时修改字节码。它是以premain
方法为入口的,premain
方法有两个重载方法:
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)
- agentArgs:字符串参数,通过
-javaagent
传递 - inst:
java.lang.instrument.Instrumentation
类对象
JVM 会优先加载带 Instrumentation
对象参数的方法,加载成功忽略第二种;如果第一种没有,则加载第二种方法。
下面写个简单的例子玩一下静态加载:
- 新建一个maven项目并引入javassist包
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.2-GA</version>
</dependency>
- 新建一个
ClassFileTransformer
接口实现类PrintMethodCostTransformer,用户打印方法耗时
public class PrintMethodCostTransformer implements ClassFileTransformer {
private final String targetClassName;
public PrintMethodCostTransformer(String targetClassName) {
this.targetClassName = targetClassName;
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
try {
if (className != null) {
className = className.replaceAll("/", ".");
if (className.contains(targetClassName)) {
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(className);
CtClass ctClass = classPool.get(className);
CtMethod[] methods = ctClass.getDeclaredMethods();
if (methods != null && methods.length > 0) {
for (CtMethod ctMethod : methods) {
String methodName = ctMethod.getName();
if (!"main".equals(methodName)) {
//修改字节码,定义两个long类型变量
ctMethod.addLocalVariable("begin", CtClass.longType);
ctMethod.addLocalVariable("end", CtClass.longType);
//方法执行前操作
ctMethod.insertBefore("System.out.println(\"进入 ["+methodName+"] 方法\");");
ctMethod.insertBefore("begin = System.nanoTime();");
//方法执行后操作
ctMethod.insertAfter("end = System.nanoTime();");
ctMethod.insertAfter("System.out.println(\"方法 [" + methodName + "] 耗时:\"+ (end - begin) +\"ns\");");
ctMethod.insertAfter("System.out.println(\"退出 ["+methodName+"] 方法\");");
}
}
ctClass.detach();
return ctClass.toBytecode();
}
}
}
} catch (Throwable e) {
e.printStackTrace();
}
return new byte[0];
}
}
- 定义一个agent代理类AgentClass
/**
* @author 索码理(suncodernote)
*/
public class AgentClass {
// main方法执行前的修改
public static void premain(String agentArgs , Instrumentation inst){
System.out.println("enter premain(String agentArgs , Instrumentation inst)");
inst.addTransformer(new PrintMethodCostTransformer(agentArgs));
}
}
修改配置文件并打包
4.1 在META-INF文件夹下文件夹下创建一个MANIFEST.MF文件
文件内容如下
Manifest-Version: 1.0
Created-By: 索码理(suncodernote)
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.example.agent.AgentClass
4.2 在修改pom.xml,指定MANIFEST.MF文件覆盖掉自动生成的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>
最后通过maven install打包成一个jar包,我这里打成的jar包完成路径为D:\Program Files\apache-maven-3.6.3\repository\BasicJava\basic_java_demos\1.0-SNAPSHOT\basic_java_demos-1.0-SNAPSHOT.jar
。一个简单的静态加载就完成了,接下来进行测试。
静态加载测试
- 新建一个maven项目,并新建一个测试类
MainTest每隔两秒钟调用一次print方法
public class MainTest {
public static void main(String[] args) throws InterruptedException {
while (true){
print("agents");
TimeUnit.SECONDS.sleep(2);
}
}
private static void print(String name){
System.out.println("时间:"+LocalDateTime.now()+","+"hello "+name);
}
}
- 在MainTest测试类添加启动参数
-javaagent:"D:\Program Files\apache-maven-3.6.3\repository\BasicJava\basic_java_demos\1.0-SNAPSHOT\basic_java_demos-1.0-SNAPSHOT.jar"=com.example.jvmlearing.agent.MainTest
上面这段代码是指定了上面打包的agent jar包的完成路径,并通过-javaagent
命令向agent包传递参数com.example.jvmlearing.agent.MainTest
。
-Xbootclasspath/a:D:\javassist-3.28.0-GA.jar
是引用javassist包,当然也可以通过maven引入到项目中
- 启动MainTest进行测试
控制台打印结果:
从结果中可以看到premain
方法是最先启动的,而且打印了方法耗时。
动态加载
将Java代理加载到已经运行的JVM中的过程称为动态加载。它利用的是Java的Attach API,Attach API不是Java标准API,而是Sun公司提供的一套扩展API,用来向目标JVM”依附”(Attach)代理工具程序的。Attach 机制对外提供了一种进程间的通信能力,能让一个进程传递命令给 JVM;其次,Attach 机制内置一些重要功能,可供外部进程调用。比如 jstack
工具就是要依赖这个机制来工作的。
动态加载的入口是agentmain
方法,同样它也有两个重载方法
public static void agentmain(String agentArgs, Instrumentation inst)
public static void agentmain(String agentArgs)
和premain
方法一样,JVM 会优先加载带 Instrumentation
对象参数的方法,加载成功忽略第二种;如果第一种没有,则加载第二种方法。
举个例子,简单玩一下动态加载:
- 在刚刚的java agent项目的AgentClass中加入
agentmain
方法
public class AgentClass {
// main方法执行前的修改
public static void premain(String agentArgs , Instrumentation inst){
System.out.println("enter premain(String agentArgs , Instrumentation inst)");
inst.addTransformer(new PrintMethodCostTransformer(agentArgs));
}
// 控制类运行时的行为
public static void agentmain(String agentArgs , Instrumentation inst) throws UnmodifiableClassException, ClassNotFoundException {
System.out.println("enter agentmain(String agentArgs , Instrumentation inst)");
inst.addTransformer(new PrintMethodCostTransformer(agentArgs) , true);
Class<?> claz = Class.forName(agentArgs);
//要重新定义的类
inst.retransformClasses(claz);
}
}
- MANIFEST.MF文件中加入agent-class
- 重新再打包
动态加载测试
- 在刚刚测试的项目中引入本地依赖tools.jar
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8.0</version>
<scope>system</scope>
<systemPath>${JAVA_HOME}\lib\tools.jar</systemPath>
</dependency>
- 新建一个测试类AttachTest
public class AttachTest {
public static void main(String[] args) throws AgentLoadException, IOException, AgentInitializationException, AttachNotSupportedException, InterruptedException {
//传递给java代理的参数
String targetClassName = "com.example.jvmlearing.agent.MainTest";
String jar = "D:\\Program Files\\apache-maven-3.6.3\\repository\\BasicJava\\basic_java_demos\\1.0-SNAPSHOT\\basic_java_demos-1.0-SNAPSHOT.jar";
for (VirtualMachineDescriptor descriptor: VirtualMachine.list()) {
String displayName = descriptor.displayName();
if (displayName.equals(targetClassName)) {
String processId = descriptor.id();
System.out.println("process id="+processId);
VirtualMachine virtualMachine = VirtualMachine.attach(processId);
//加载代理jar
virtualMachine.loadAgent(jar, targetClassName);
//解除绑定
virtualMachine.detach();
}
}
}
}
- 删除测试类MainTest的启动参数并先后启动MainTest和AttachTest
可以看到agentmain
方法是在MainTest运行时执行的。
启动AttachTest之后,可以看到AttachTest的控制台打印的MainTest的进程id
再通过jps
命令看下MainTest的进程id
可以看到两个进程id是一样的,也说明动态加载依附的是虚拟机进程。
Attach API 很简单,只有2个主要的类,都在 com.sun.tools.attach
包里面:
- VirtualMachine类:代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了获取系统信息(比如内存dump、线程dump,类信息统计)、加载代理程序、Attach 和 Detach 等方法 。
- VirtualMachineDescriptor类:一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。
通过VirtualMachine类的attach(pid)
方法,便可以attach到一个运行中的java进程上,之后便可以通loadAgent(agentJarPath)
来将agent的jar包注入到对应的进程,然后对应的进程会调用agentmain
方法。
静态加载和动态加载的区别
从上面的例子中可以发现静态加载是需要和被代理的程序一起启动,需要在启动的时候通过-javaagent
参数指定静态加载的jar包,被代理的程序是“知道”自己被代理的。而动态加载则需要被代理程序先启动,只要获取到被代理程序的进程id,通过loadAgent
方法指定动态加载jar包就行了,它属于热插拔式的。
总结
本篇文章我们分别使用Java Agent的静态加载和动态加载成功的对字节码进行了修改、追踪,并完成了一个打印方法耗时的简单示例。Java Agent能够访问加载到JVM中的类,它的应用十分广泛,可用于实现Java IDE的调试功能、热部署功能、线上诊断⼯具和性能分析⼯具。本篇只是触及了Java Agent的皮毛,感兴趣的可以深入了解一下。下篇文章将介绍一个动态追踪框架BTrace。
参考资料: https://www.baeldung.com/java-instrumentation https://www.developer.com/design/what-is-java-agent/ https://www.jianshu.com/p/6967d4dfbc49
相关文章
- MySQL字段类型如何转为java_Java JDBC中,MySQL字段类型到JAVA类型的转换
- java redis锁_Java中Redis锁的实现[通俗易懂]
- Java 八大基本数据类型
- java 阶乘算法_Java 实现阶乘算法
- Java课程设计之 学生成绩管理系统「建议收藏」
- 【说站】java方法参数中通配符的使用
- JAVA对象转map_java处理字符串类型的map
- java输出一个数组的元素_Java输出数组元素「建议收藏」
- java+sm4+加密算法_SM4加密算法实现Java和C#相互加密解密
- jmeter基本使用方法面试题目_java 面试 高质量 集合面试问题
- java中strictfp关键字,java strictfp关键字用法大全详解
- Java web-httpservlet中cookie的用法
- Java 微服务
- Java压缩或放大图像处理详解编程语言
- 俄罗斯方块游戏完整Java源码详解编程语言
- Java 根据日期计算星期几详解编程语言
- 深入Java:利用API快速创建MySQL表(java创建mysql表)
- 使用Java在Linux系统上开发:一步步安装全部组件(java环境linux)
- Linux 卸载Java:简单步骤完成(linux卸载java)
- 实现使用Java实现Redis消息队列(redis消息队列java)
- MySQL连接Java:一步一步实现连接(mysql连接java)
- 玩转Linux:Java开发入门指南(linux上开发java)
- 设置Redis Java: Setting Expiration Time.(redisjava过期)
- 数据解决Redis和Java中数据过期问题(redisjava过期)
- 时间Java操作Redis实现设置Key的过期时间(redisjava过期)
- Java并发编程视频分享-第二期
- Java运行在Linux系统上免费下载(linux java下载)
- Linux下Java命令:简介与基本用法(linux下java命令)
- Java与MySQL事务处理(mysql中java事物)
- 通过spring用beanshell实现java接口示例