Java Agent学习
参考文章:
附录
代码仓库:GitHub - baiyz0825/java-agent-demo: 示范演示java-agent的基本demo和遇到的问题解决
PPT:[[[Java Agent技术深度解析与实战应用.pptx]]]
基本概念
本质上就是一种Java程序,目的是希望在Java程序启动时或者运行时候动态修改、监测Class字节码文件,方便调试监控等。
Java Agent 是一种基于 java.lang.instrument 包的技术,可以在 JVM 启动时或运行时加载代理代码,修改目标类的字节码。它主要用于 AOP(面向切面编程)、性能监控、日志收集、测试工具、应用安全等场景。
历史起源
JVMTI
JVMTI(JVM Tool Interface)是Java虚拟机对外提供的Native编程接口,通过JVMTI,外部进程可以获取到运行时JVM的诸多信息,比如线程、GC等。
Instrumentation
Instrumentation接口设计初衷是为了收集Java程序运行时的数据,用于监控运行程序状态,记录日志,分析代码用的。
- java.lang.instrument (Java Platform SE 8 )
在Java SE 5之前,要实现一个Agent只能通过编写Native代码来实现。
从Java SE 5开始,可以使用Java的Instrumentation接口(java.lang.instrument包)来编写Agent。
从而 Java SE 6 的新特性改变了这种情况,通过 Java Tool API 中的 attach 方式,我们可以很方便地在运行过程中动态地设置加载代理类,以达到Instrumentation的目的。并且从Java SE 6开始,可以向native method插桩。
本质上:它们的工作都是借助JVMTI来进行完成
/** |
addTransformer(ClassFileTransformer transformer, boolean canRetransform- 用途: 注册类文件转换
- 功能: 将转换器添加到JVM中,所有未来的类定义都会经过该转换器处理
- 参数:
- transformer: 类文件转换器
- canRetransform: 是否支持重转换
addTransformer(ClassFileTransformer transformer)- 用途: 注册类文件转换器(简化版)
- 功能: 等同于 addTransformer(transformer, false)
removeTransformer(ClassFileTransformer transformer)- 用途: 注销类文件转换器
- 功能: 从JVM中移除指定的转换器
isRetransformClassesSupported()- 用途: 检查是否支持类重转换
- 功能: 返回当前JVM配置是否支持对已加载类进行重转换
retransformClasses(Class<?>... classes)- 用途: 重转换指定的类
- 功能: 对已加载的类重新运行转换过程,应用新的字节码修改
isRedefineClassesSupported()- 用途: 检查是否支持类重定义
- 功能: 返回当前JVM配置是否支持类重定义功能
redefineClasses(ClassDefinition... definitions)- 用途: 重定义指定的类
- 功能: 使用新的类文件字节码替换类的定义,用于热修复等场景
isModifiableClass(Class<?> theClass)- 用途: 检查类是否可修改
- 功能: 判断指定的类是否可以通过重转换或重定义进行修改
getAllLoadedClasses()- 用途: 获取所有已加载的类
- 功能: 返回JVM中当前加载的所有类和接口的数组
getInitiatedClasses(ClassLoader loader)- 用途: 获取类加载器初始化的类
- 功能: 返回指定类加载器能够通过名称找到的所有类
getObjectSize(Object objectToSize)- 用途: 估算对象大小
- 功能: 返回对象占用的存储空间近似值(实现相关)
appendToBootstrapClassLoaderSearch(JarFile jarfile)- 用途: 添加到引导类加载器搜索路径
- 功能: 将JAR文件添加到引导类加载器的搜索路径中
appendToSystemClassLoaderSearch(JarFile jarfile)- 用途: 添加到系统类加载器搜索路径
- 功能: 将JAR文件添加到系统类加载器的搜索路径中
isNativeMethodPrefixSupported()- 用途: 检查是否支持本地方法前缀设置
- 功能: 返回当前JVM是否支持为本地方法设置前缀
setNativeMethodPrefix(ClassFileTransformer transformer, String prefix)- 用途: 设置本地方法前缀
- 功能: 为指定的转换器设置本地方法前缀,用于包装本地方法
redefineModule() (Java 9+)- 用途: 重定义模块
- 功能: 扩展模块的读取集、导出包、开放包或服务提供
isModifiableModule(Module module) (Java 9+)- 用途: 检查模块是否可修改
- 功能: 判断指定的模块是否可以通过重定义进行修改
运行模式
主程序可以指定的agent的数量是没有任何限制的,但是会根据指定的先后顺序依次执行各个agent的逻辑。但是agent抛出异常,会导致主程序的启动失败。
Java Agent 可以在两种模式下运行:
- 启动时代理 (premain) :在 JVM 启动时加载。
exp:java -javaagent:/path/to/agent.jar -jar yourApp.jar - 运行时代理 (agentmain) :在 JVM 启动后动态加载。
下面是对应的arthas调用attach的方法 arthas/core/src/main/java/com/taobao/arthas/core/Arthas.java at aeb9c9d1e9c8732378ef18f51e7043d858526a38 · alibaba/arthas · GitHub

原理
静态加载过程
agent 中的 class 由 system calss loader(默认AppClassLoader) 加载,premain() 方法会调用 Instrumentation API,然后 Instrumentation API 调用 JVMTI(JVMTI的内容将在后面补充),在需要加载的类需要被加载时,会回调 JVMTI,然后回调 Instrumentation API,触发ClassFileTransformer.transform(),最终修改 class 的字节码。
![[Agent基本原理 2025-12-01 19.59.25.excalidraw]]
Instrument agent启动时加载会实现Agent_OnLoad方法,具体实现逻辑如下:
- 创建并初始化JPLISAgent
- 监听VMInit事件,在vm初始化完成之后执行下面逻辑
- 创建Instrumentation接口的实例,也就是InstrumentationImpl对象
- 监听ClassFileLoadHook事件(类加载事件)
- 调用InstrumentationImpl类的loadClassAndCallPremain方法,这个方法会调用javaagent的jar包中里的MANIFEST.MF里指定的Premain-Class类的premain方法
- 解析MANIFEST.MF里的参数,并根据这些参数来设置JPLISAgent里的内容
应用场景

- 性能监控和调优:
- 通过在运行时收集类加载信息、方法执行时间、内存使用情况等,可以用于监控应用的性能。
- 实现更深入的性能分析工具,如探查器(Profiler),来定位性能瓶颈。
- 日志增强:
- 在不修改源代码的情况下,为已有系统添加或修改日志记录逻辑。
- 可以动态地向特定的方法中插入日志输出语句,帮助调试和跟踪问题。
- 字节码操作和AOP(面向切面编程):
- 动态地在方法执行前后插入额外的逻辑,实现横切关注点(cross-cutting concerns),比如事务管理、安全检查等。
- 热修复(Hotfixing):
- 在生产环境中直接修改正在运行的应用程序的行为,而不需要重新部署或重启服务。
- 通过修改字节码来临时解决紧急的问题,等待正式修复版本上线。
- 依赖注入框架的支持:
- 部分依赖注入框架可能利用 Java Agent 来实现在运行时自动配置和管理 Bean 的功能。
- 安全性加强:
- 实施额外的安全措施,例如加密敏感数据、防止反序列化攻击等。
- 监控并限制某些潜在危险的操作,提升系统的安全性。
- 资源管理:
- 自动管理资源释放,确保没有泄露发生,比如数据库连接、文件句柄等。
- 测试支持:
- 提供了在测试阶段对应用程序内部状态进行检查的能力,有助于编写更加健壮的测试用例。
- 模拟异常条件,验证系统的容错能力。

**
常见Java Agent框架
| 框架 | 类型 | 是否可直接用于 Java Agent | 是否需要 -javaagent | 特点 | 典型用途 |
|---|---|---|---|---|---|
| ByteBuddy | 字节码操作库(高级) | ✅ 是(推荐) | ✅ 是(或通过 attach) | API 简洁、类型安全、性能好、支持 agent 构建器 | APM(如 SkyWalking)、Mock 框架、动态代理 |
| ASM | 字节码操作库(底层) | ✅ 是 | ✅ 是 | 性能极高、控制精细、学习曲线陡 | 编译器、框架底层(如 Javassist、CGLIB 底层) |
| Javassist | 字节码操作库(高层) | ✅ 是 | ✅ 是 | 用 Java 源码字符串操作字节码,易上手 | 快速原型、调试工具(如 Arthas)、教学 |
| Spring Instrument | Spring 提供的 Agent 工具 | ⚠️ 间接使用 | ✅ 是(仅用于 LTW) | 专为 Spring Load-Time Weaving 设计 | 在 Spring 中启用 AspectJ LTW |
这里用于编写agent比较多的是ByteBuddy api简单
基本示例
使用 javaassitant
demo: java-agent-demo/bytebuddy-agent at main · baiyz0825/java-agent-demo · GitHub
使用 bytebuddy
demo:java-agent-demo/javassist-agent at main · baiyz0825/java-agent-demo · GitHub
类加载原理
[[Jvm类加载原理]]
潜在问题
依赖冲突
agent中使用版本和实际业务代码不同
- 同包API不兼容: 类加载顺序有关,在使用agent的时候一般是默认使用appClassLoader进行加载的
- classNotFound:应用程序中不包含这个agent的依赖,增强代码是Agent提供,这部分代码与用户代码结合在一起,被AppClassLoader加载。不希望业务也引用这部分依赖代码,不侵入业务。
示例
下面用不同版本的gson来表现这里的问题:
| 组件 | 版本 | 说明 |
|---|---|---|
| demo-app 使用的 Gson | 2.2.4 | 较旧版本,API有限 |
| conflict-failure-demo Agent | 2.8.9 | 最新版本,API丰富 |
┌─────────────────────────────────────────┐ |
解决方案
委托模式
demo代码:java-agent-demo/conflict-dispatcher-demo at main · baiyz0825/java-agent-demo · GitHub
将 Action 接口和 Dispatcher 注入到 Bootstrap ClassLoader,最终使用Dispatcher 将 Agent 依赖传递给应用代码
基本架构
┌─────────────────────────────────────────────────────────────────┐ |
工作流程
![[Java Agent 2025-12-01 21.23.30.excalidraw]]
Action 接口(注入到 Bootstrap)
public interface Action { |
maven shade
demo代码:java-agent-demo/conflict-shade-demo at main · baiyz0825/java-agent-demo · GitHub
通过 Maven Shade 插件将依赖的包路径重命名 ,Agent 和应用可以使用不同版本的同名库 ,应用代码无需任何修改

实现的基本架构:
编译前 Shade 后 |
工作流程:
┌────────────────────────────────────────────────────────────────┐ |
替换了引用类的路径,自己加载自己部分的
核心配置
| 配置项 | 说明 | |
|---|---|---|
shadedArtifactAttached |
是否将 shaded JAR 作为附加产物 | |
shadedClassifierName |
shaded JAR 的分类器名称 | |
relocations |
包重命名规则 | |
pattern |
原始包名 | |
shadedPattern |
重命名后的包名 |
<plugin> |
缺点
- 必须将所有依赖涉及的类进行重定位及对资源进行排除,且由于 Maven 依赖的传递性,那么需要将依赖引入的依赖树涉及的类都进行重定位,如果 Java Agent 中的依赖重定位声明漏掉了一些类,那么就和我们开始提到的情况一样,在应用依赖的版本不一致的情况下,容易出现类转换等异常
- 重命名包,会导致不容易进行Debug
自定义类加载器
demo:java-agent-demo/conflict-bootstrap-demo at main · baiyz0825/java-agent-demo · GitHub
- 自定义 ClassLoader:创建 Parent 为 Bootstrap ClassLoader 的自定义类加载器
- 依赖隔离:Agent 依赖与应用依赖完全隔离
- 全局可见:Bootstrap 级别的类对所有 ClassLoader 可见
- 无包重命名:保持原始包名,便于调试
基本架构
┌─────────────────────────────────────────────────────────────────┐ |
上述描述的隔离原理:
- 创建 CustomBootstrapClassLoader:Parent 设置为
null(即 Bootstrap ClassLoader) - 加载 Agent 依赖:通过 CustomBootstrapClassLoader 加载 Gson 等依赖
- 类隔离:
- Agent 的 Gson 由 CustomBootstrapClassLoader 加载
- 应用的 Gson 由 System ClassLoader 加载
- 两者是不同的类(由不同 ClassLoader 加载的同名类是不同的类)
工作流程
// 1. 创建自定义 ClassLoader |
QA
Q1: 为什么要用反射?
不同 ClassLoader 加载的同名类是不同的类型:
// CustomBootstrapClassLoader 加载的 Gson |
