Java Agent使用
Java Agent 是一种特殊的 Java 程序,能够在JVM启动时或运行时对 Java 应用程序进行监控、分析和修改。Java Agent 主要用于以下几个方面:
- 字节码操作:使用库如 ASM 或 Javassist,可以在类加载时修改类的字节码。这使得开发者可以在不修改源代码的情况下改变类的行为。
- 性能监控:Java Agent 可以用于监控应用程序的性能,例如跟踪方法调用、监测内存使用情况等,常用于 APM(应用性能管理)。
- 调试和测试:可以在运行时插入调试信息或测试代码,以帮助开发者找到问题。
基本使用
Agent程序支持目标JVM在启动时加载和运行时加载。这两种不同的加载模式会使用不同的入口函数:
- 在应用启动之时,通过
premain()方法来实现在应用启动时侵入 - 针对运行中的JVM,通过Attach API和
agentmain()方法来实现侵入
在启动 Java 应用程序时,使用 -javaagent 参数指定需要加载的agent程序:java -javaagent:path/to/your-agent.jar -jar your-application.jar。
JVM应用在启动时,如果指定了agent,则会优先加载Agent这个jar包,并执行premain或者agentmain方法,这时其他的类都还没有被加载,可以实现对新加载的类进行字节码修改,但如果premain方法或者agentmain方法执行失败或者抛出异常,则JVM会被终止。
主程序可以指定的agent的数量是没有任何限制的,但是会根据指定的先后顺序依次执行各个agent的逻辑。
java -javaagent:agent1.jar -javaagent:agent2.jar -jar your-application.jar
# 传递参数,参数应以 key=value 的形式传递,并用逗号分隔
java -javaagent:path/to/your-agent.jar=param1=value1,param2=value2 -jar your-application.jar
相关命令行参数:通过java -help查看
-agentlib:<libname>[=<options>]
load native agent library <libname>, e.g. -agentlib:hprof
see also, -agentlib:jdwp=help and -agentlib:hprof=help
-agentpath:<pathname>[=<options>]
load native agent library by full pathname
-javaagent:<jarpath>[=<options>]
load Java programming language agent, see java.lang.instrument
编写Agent程序
写Agent程序,需要实现下面的方法之一:
public static void premain(String agentArgs)
public static void premain(String agentArgs, Instrumentation inst)
public static void agentmain(String agentArgs)
public static void agentmain(String agentArgs, Instrumentation inst)
第1个参数agentArgs是随同 –javaagent 一起传入的程序参数,如果这个字符串代表了多个参数,就需要自己解析这些参数。
第2个参数inst是Instrumentation类型的对象,由JVM自动传入的,可以拿这个参数进行类增强等操作。
注意:
- premain和agentmain是2组方法,一般只实现其中一组中的一个方法即可。这两种的区别是: premain是在jvm启动的时候类加载到虚拟机之前执行的,而agentmain是在要代理的程序的main方法执行后加载的,也就是运行时加载agent。agentmain这种方式会转换会有一些限制,比如不能增加或移除字段,这种模式称为attach模式。
- 带Instrumentation参数和不带Instrumentation参数的两个方法都存在时,带有Instrumentation参数的方法的优先级更高,会被JVM优先加载。
启动时加载-premain
写一个只包含一个方法的入口类,方法名必须是premain
package org.example;
import java.lang.instrument.Instrumentation;
public class PreMainAgent {
public static void premain(String agentArgs, Instrumentation inst) {
// 这里可以插入代码以修改字节码
System.out.println(agentArgs);
}
}
打包
agent程序需要打包成一个jar包,并且需要确保在生成的 jar 包中的 MANIFEST.MF 文件中指定Agent程序入口类的全限定类名:
-
如果实现premain,需要添加Premain-Class: org.example.PreMainAgent
-
如果实现agentmain,需要添加Agent-Class: org.example.PreMainAgent
可以直接使用maven来进行打包:执行mvn package
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<finalName>myagent</finalName>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<!--指定包含premain方法的类,需要配置为类的全路径名,必须配置-->
<Premain-Class>org.example.PreMainAgent</Premain-Class>
<!--指定包含agentmain方法的类,需要配置为类的全路径名,必须配置-->
<!-- <Agent-Class>org.example.AgentMain</Agent-Class> -->
<!--是否可以重新定义class,默认为false,可选配置-->
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<!--是否可以重新转换class,实现字节码替换,默认为false,可选配置-->
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<!--是否可以设置Native方法的前缀,默认为false,可选配置-->
<Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
打包后就制作好了agent程序了,只需要在想要修改的应用程序启动时指定这个agent jar包就可以了。
打包后的META-INF/MANIFEST.MF文件大概是这样
Manifest-Version: 1.0
Premain-Class: org.example.PreMainAgent
Built-By: xxx
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Can-Set-Native-Method-Prefix: true
Created-By: Apache Maven 3.9.6
Build-Jdk: 1.8.0_361
如果没装maven,这里也提供一种不使用maven的方式:
# 写入MANIFEST.MF文件
mkdir META-INF && touch META-INF/MANIFEST.MF
# 手动创建文件也可以
cat <<EOL > META-INF/MANIFEST.MF
Manifest-Version: 1.0
Premain-Class: org.example.PreMainAgent
Built-By: xxx
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Can-Set-Native-Method-Prefix: true
EOL
# 编译并打包
javac ./PreMainAgent.java && jar cfm myagent.jar META-INF/MANIFEST.MF PreMainAgent.class
现有一个应用程序
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
System.out.println(Arrays.toString(args));
}
}
编译运行此程序:javac ./Main.java
java -javaagent:./myagent.jar=param1=value1,param2=value2 -Xms512m -Xmx1024m Main param3 param4
结果如下:
[root@localhost java]# java -javaagent:./myagent.jar=param1=value1,param2=value2
\ -Xms512m -Xmx1024m Main param3 param4
param1=value1,param2=value2
[param3, param4]
运行时加载-agentmain
前面已经演示了premain的用法,现在来看下agentmain是如何使用的
先写一个使用agentmain的agent程序
public class AgentMainAgent {
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println(agentArgs);
}
}
运行时不能通过添加启动参数的方式来连接agent和主程序,借助tools.jar,这个jar包一般在JDK8中自带的。运行时加载这个动作可以由应用程序主动加载,或者由其他VM实例来为指定的JVM实例加载代理。下面给一个应用程序主动加载的例子:
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.lang.management.ManagementFactory;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) throws Exception {
// 获取当前 JVM 的进程 pid
String name = ManagementFactory.getRuntimeMXBean().getName();
// name 格式为 "<pid>@<hostname>"
String pid = name.split("@")[0];
System.out.println("current jvm pid is: " + pid);
TimeUnit.SECONDS.sleep(2);
System.out.println("try to load agent " + args[0] + args[1]);
attachAgentToTargetJVM(pid, args[0], args[1]);
System.out.println("load " + args[0] + " successfully.");
}
public static void attachAgentToTargetJVM(String targetVmPid,
String agent, String agentArgs) throws Exception {
// 获取运行的JVM实例
List<VirtualMachineDescriptor> virtualMachineDescriptors = VirtualMachine.list();
VirtualMachineDescriptor targetVM = null;
for (VirtualMachineDescriptor descriptor : virtualMachineDescriptors) {
if (descriptor.id().equals(targetVmPid)) {
targetVM = descriptor;
break;
}
}
if (targetVM == null) {
throw new IllegalArgumentException("could not find the target jvm by process id:" +
targetVmPid);
}
VirtualMachine virtualMachine = null;
try {
virtualMachine = VirtualMachine.attach(targetVM);
virtualMachine.loadAgent(agent, agentArgs);
} catch (Exception e) {
if (virtualMachine != null) {
virtualMachine.detach();
}
}
}
}
获取到VirtualMachine实例后,可以通过loadAgent方法可以实现注入agent代理类的操作,该方法的第一个参数是agent的jar路径,第二个参数是传给agnet的参数。
同样的,编译运行,结果如下:
javac Main.java
# 如果编译报错,需要添加tools.jar到编译类路径下:javac -cp /path/to/tools.jar Main.java
java -Xms512m -Xmx1024m Main ./myagent.jar param1=value1,param2=value2
[root@localhost java]# java -Xms512m -Xmx1024m Main ./myagent.jar param1=value1,param2=value2
current jvm pid is: 1049294
try to load agent ./myagent.jarparam1=value1,param2=value2
param1=value1,param2=value2
load ./myagent.jar successfully.
低版本的JDK需要单独引入tools.jar包,如果是高版本的JDK则不需要引入单独引入tools.jar包。如果缺少tools.jar包,添加maven依赖:将tools.jar包放到本地文件并指定其为system依赖
<!-- https://mvnrepository.com/artifact/com.sun/tools -->
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${project.basedir}/libs/tools.jar</systemPath>
</dependency>
上面的agent只是简单示范了下怎么写agent程序,没有实现什么功能,可以基于这个示范使用Instrumentation的一些API实现自己想要的功能。
Attach API
Instrumentation
java.lang.instrument.Instrumentation 接口是 Attach API 的核心接口
主要功能是进行字节码修改,通过提供的 redefineClasses 方法,可以在运行时重新定义已加载的类。这使得开发者能够在不重启应用程序的情况下,修复错误或添加功能。
通过 addTransformer 方法,可以注册一个类转换器(ClassFileTransformer),该转换器在每次类加载时被调用,从而允许开发者在类加载之前对字节码进行修改,同时提供了其他方法获取 JVM 的一些运行时信息,比如已加载的类、JVM 版本等。
常用方法
- addTransformer(ClassFileTransformer transformer):注册一个新的类转换器,用于在类加载时修改字节码。
- redefineClasses(ClassDefinition... definitions):重新定义已加载的类的字节码。
- getAllLoadedClasses():返回当前 JVM 中所有加载的类的数组。
- isModifiableClass(Class<?> theClass):检查指定的类是否可以被修改。
ClassFileTransformer
ClassFileTransformer 是 Java Instrumentation API 中的一个接口,用于在类加载时对字节码进行转换。通过实现这个接口,你可以修改正在加载的类的字节码,例如添加方法、修改方法体或改变字段。
byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer);
参数说明
- ClassLoader loader: 类加载器,用于加载当前类的类。
- String className: 正在加载的类的名称(用斜杠分隔的形式,例如 java/lang/String)。
- Class<?> classBeingRedefined: 如果这个类正在被重新定义,则为其对应的 Class 对象;否则为 null。
- ProtectionDomain protectionDomain: 类的保护域,提供关于类的安全权限的信息。
- byte[] classfileBuffer: 原始的字节码数组。
返回修改后的字节码数组。如果不想修改字节码,可以返回原始的 classfileBuffer。
下面是一个例子:
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
public class MyTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
// 在这里可以对字节码进行修改
if ("my/package/MyClass".equals(className)) {
// 例如,修改 MyClass 的字节码
System.out.println("Transforming class: " + className);
// 这里可以使用字节码操作库(如 ASM 或 Javassist)来修改字节码
// 返回修改后的字节码或原始字节码
}
return classfileBuffer; // 如果不修改,返回原始字节码
}
}
版本问题
Attach API是com.sun提供的API,如果使用其他厂商的JVM,可能没有这个API了,比如Eclipse的J9,在J9中使用Attach API可以参考https://www.ibm.com/docs/en/sdk-java-technology/8?topic=documentation-java-attach-api
关于tools.jar
java8及之前tools.jar随jdk一同安装

java8之后,tools.jar被移除,参考JEP 220
premain和agentmain对比
premain 只能在类加载之前修改字节码,类加载之后无能为力,只能通过重新创建ClassLoader 这种方式重新加载。而 agentmain 可以在类加载之后再次加载一个类,也就是重定义,你就可以通过在重定义的时候进行修改类了,甚至不需要创建新的类加载器,JVM 已经在内部对类进行了重定义,重定义的过程相当复杂。
premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
这种方式对类的修改是由限制的,对比原来的类,有如下要求:
- 新类和老类的父类必须相同;
- 新类和老类实现的接口数也要相同,并且是相同的接口;
- 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致;
- 新类和老类新增或删除的方法必须是private static/final修饰的;
- 字段数和字段名必须一致;
- 可以删除修改方法体;
相比较重新创建类加载器,限制还是挺多的,最重要的字段是无法修改的。因此,使用的时候要注意。
除了上面的方式,如果想要重新定义一个类,可以考虑基于类加载器隔离的方式:创建一个新的自定义类加载器去通过新的字节码去定义一个全新的类,但是这种方式只能通过反射调用该全新类。
参考资料

浙公网安备 33010602011771号