javaagent入门
介绍
java agent是一种java代理技术,JDK1.5引入,支持动态修改Java字节码。
功能和应用场景
IDE 的调试功能,例如 Eclipse、IntelliJ IDEA ;
热部署功能,例如 JRebel、XRebel、spring-loaded;
各种线上诊断工具,例如 Btrace、Greys,还有阿里的 Arthas;
各种性能分析工具,例如 Visual VM、JConsole 等;
全链路性能检测工具,例如 Skywalking、Pinpoint等;
原理
JVMTI
JVMTI (JVM Tool Interface)是 Java 虚拟机对外提供的 Native 编程接口,通过 JVMTI ,外部进程可以获取到运行时JVM的诸多信息,比如线程、GC等。
JVMTI 是一套 Native 接口,在 Java SE 5 之前,要实现一个 Agent 只能通过编写 Native 代码来实现。从 Java SE 5 开始,可以使用 Java 的Instrumentation 接口(java.lang.instrument)来编写 Agent。无论是通过 Native 的方式还是通过 Java Instrumentation 接口的方式来编写 Agent,它们的工作都是借助 JVMTI 来进行完成。JVMTI和Instumentation API的作用很相似,都是一套 JVM 操作和监控的接口,且都需要通过agent来启动:
- Instumentation API需要打包成 jar,并通过 Java agent 加载(对应启动参数: -javaagent)
- JVMTI 需要打包成动态链接库(随操作系统,如.dll/.so文件),并通过 JVMTI agent 加载(对应启动参数: -agentlib/-agentpath)
JVMTIAgent
是一个动态库,利用JVMTI暴露出来的一些接口来干一些我们想做、但是正常情况下又做不到的事情,不过为了和普通的动态库进行区分,它一般会实现如下的一个或者多个函数:
- Agent_OnLoad函数,如果agent是在启动时加载的,通过JVM参数设置
- Agent_OnAttach函数,如果agent不是在启动时加载的,而是我们先attach到目标进程上,然后给对应的目标进程发送load命令来加载,则在加载过程中会调用Agent_OnAttach函数
- Agent_OnUnload函数,在agent卸载时调用
JVM Attach
是指 JVM 提供的一种进程间通信的功能,能让一个进程传命令给另一个进程,并进行一些内部的操作,比如进行线程 dump,那么就需要执行 jstack 进行,然后把 pid 等参数传递给需要 dump 的线程来执行,原理这篇文章讲的很详细:https://juejin.cn/post/7157684112122183693#heading-27
Instrumentation
实现了Agent_OnLoad和Agent_OnAttach两方法,也就是说在使用时,agent既可以在启动时加载,也可以在运行时动态加载。其中启动时加载还可以通过类似-javaagent:jar包路径的方式来间接加载instrument agent,运行时动态加载依赖的是JVM的attach机制,通过发送load命令来加载agent。
Instrumentation是Java提供的JVM接口,该接口提供了一系列查看和操作Java类定义的方法,例如修改类的字节码、向 classLoader 的 classpath 下加入jar文件等。使得开发者可以通过Java语言来操作和监控JVM内部的一些状态,进而实现Java程序的监控分析,甚至实现一些特殊功能(如AOP、热部署)。
主要方法
addTransformer方法配置之后,后续的类加载都会被Transformer拦截。
对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
public interface Instrumentation {
/**
* 注册一个Transformer,从此之后的类加载都会被Transformer拦截。
* Transformer可以直接对类的字节码byte[]进行修改
*/
void addTransformer(ClassFileTransformer transformer);
/**
* 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。
* retransformClasses可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
*/
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
/**
* 获取一个对象的大小
*/
long getObjectSize(Object objectToSize);
/**
* 将一个jar加入到bootstrap classloader的 classpath里
*/
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
/**
* 获取当前被JVM加载的所有类对象
*/
Class[] getAllLoadedClasses();
}
- 最常用的方法是addTransformer(ClassFileTransformer transformer), 其参数ClassFileTransformer接口定义:
public interface ClassFileTransformer {
/**
* 传入参数表示一个即将被加载的类,包括了classloader,classname和字节码byte[]
* 返回值为需要被修改后的字节码byte[]
*/
byte[]
transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException;
}
局限性
通过Instrumentation的redefineClasses方法进行类重定义,在redefineClasses方法上有一段注释需要特别注意:
不可以增加、删除或者重命名字段和方法,改变方法的签名或者类的继承关系。认识到这一点很重要,当我们通过ASM获取到增强的字节码之后,如果增强后的字节码没有遵守这些规则,那么调用redefineClasses方法来进行类的重定义就会失败。
java agent
主流的JVM都对Instrumentation进行了实现,但不直接提供在JDK的runtime里,而必须通过java agent。
Java agent是一种特殊的Java程序(Jar文件),它是Instrumentation的客户端。与普通Java程序通过main方法启动不同,agent 并不是一个可以单独启动的程序,而必须依附在一个Java应用程序(JVM)上,与它运行在同一个进程中,通过Instrumentation API与虚拟机交互。
premain和agentmain
JVM将首先寻找[1],如果没有发现[1],再寻找[2]。
- JVM启动时加载的是premain(静态加载)
[1] public static void premain(String agentArgs, Instrumentation inst);
[2] public static void premain(String agentArgs);
- JVM运行时加载的是agentmain(动态加载)
[1] public static void agentmain(String agentArgs, Instrumentation inst);
[2] public static void agentmain(String agentArgs);
告示文件MANIFEST.MF
在resources下创建META-INF/MANIFEST.MF文件, 或者在Maven中配置编译打包的插件
告示文件MANIFEST.MF参数说明:
Manifest-Version
文件版本
Premain-Class
包含 premain 方法的类(类的全路径名)main方法运行前代理
Agent-Class
包含 agentmain 方法的类(类的全路径名)main开始后可以修改类结构
Boot-Class-Path
设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。(可选
Can-Redefine-Classes true
表示能重定义此代理所需的类,默认值为 false(可选)
Can-Retransform-Classes true
表示能重转换此代理所需的类,默认值为 false (可选)
Can-Set-Native-Method-Prefix true
表示能设置此代理所需的本机方法前缀,默认值为 false(可选)
...
- maven plugin配置示例
<!-- 设置manifest配置文件-->
<manifestEntries>
<!--Premain-Class: 代表 Agent 静态加载时会调用的类全路径名。-->
<Premain-Class>demo.MethodAgentMain</Premain-Class>
<!--Agent-Class: 代表 Agent 动态加载时会调用的类全路径名。-->
<Agent-Class>demo.MethodAgentMain</Agent-Class>
<!--Can-Redefine-Classes: 是否可进行类定义。-->
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<!--Can-Retransform-Classes: 是否可进行类转换。-->
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
- 生成的MANIFEST.MF,示例:
Premain-Class: demo.MethodAgentMain
Built-By: xxx
Agent-Class: demo.MethodAgentMain
Can-Redefine-Classes: true
Can-Retransform-Classes: true
静态启动参数
-javaagent:<jarpath>[=<option>]
option指定 agent 的参数,可以传递到premain(String agentArgs, Instrumentation inst)方法的agentArgs入参中。支持可以定义多个agent,按指定顺序先后执行。
示例: java -javaagent:agent1.jar=key1=value1&key2=value2 -javaagent:agent2.jar -jar Test.jar
其中加载顺序为(1) agent1.jar (2) agent2.jar。
注意:不同的顺序可能会导致 agent 对类的修改存在冲突,在实际项目中用到了pinpoint和SkyWalking的agent,当通过-javaagent先挂载 pinpoint的 agent ,后挂载 SkyWalking的 agent,出现 SkyWalking对类的增强发生异常的情况,而先挂载SkyWalking的 agent 则无问题。
agent1.jar 的premain(String agentArgs, Instrumentation inst)方法的agentArgs值为key1=value1&key2=value2。
Ref
【JVM】Java agent超详细知识梳理
Java Agent 探针技术
javaagent使用指南
javaagent实战
破解 Java Agent 探针黑科技!
字节码增强技术探索
JVM源码分析之javaagent原理完全解读
javaagent-example-demo
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?