java.lang.instrument 是 JVM 提供的字节码介入机制,支持通过 premain 或 agentmain 注册 ClassFileTransformer 在类加载前修改字节码;需用 -javaagent 启动,注意白名单过滤、避免递归加载和异常抛出。

java.lang.instrument 是什么,它能干什么
它不是用来“监控类加载”这件事本身,而是提供一种在 JVM 启动或运行时,让代码介入字节码处理过程的机制。真正能感知类加载的,是 ClassFileTransformer 接口配合 premain 或 agentmain 入口 —— 类还没被定义进 JVM,你就有机会看到甚至改它。
常见错误现象:NoClassDefFoundError 或 ClassNotFoundException 在 agent 加载后突然出现,往往是因为 transformer 把某个类的字节码搞坏了,或者没正确委托给原始 ClassLoader。
- 必须用
-javaagent:path/to/agent.jar启动,否则Instrumentation实例根本不会注入 -
premain在 main 方法前执行,适合做启动期埋点;agentmain需要 attach 到运行中进程(如用VirtualMachine),适合动态增强 - 所有 transformer 默认对所有类生效,务必用
className.startsWith("com.example.")这类白名单过滤,否则连java.lang.Object都可能被误处理
怎么写一个最简可用的 premain agent
核心就三件事:打包 MANIFEST.MF、写入口方法、注册 transformer。不搞字节码修改,先做到“能打印出加载的类名”就说明链路通了。
示例关键片段:
立即学习“Java免费学习笔记(深入)”;
public class TraceAgent {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
if (className != null && className.startsWith("com.myapp.")) {
System.out.println("[Loaded] " + className.replace('/', '.'));
}
return null; // 返回 null 表示不修改字节码
}
}, true); // true 表示也对已加载类 retransform
}
}
-
MANIFEST.MF必须含Premain-Class: TraceAgent,且换行符为\r\n(Windows 下容易错成\n,导致 agent 不触发) - 打包要用
jar cvfm agent.jar MANIFEST.MF *.class,不能只用jar cf,否则 MANIFEST 会被覆盖 -
transform方法里别 throw 异常,哪怕只是printStackTrace(),否则整个类加载会失败
retransformClasses 出现 UnsupportedOperationException 怎么办
这是 JDK 默认策略限制:只有 HotSpot VM 且开启了 -XX:+HotswapAgent(旧版)或更常见的是未启用 -XX:+EnableDynamicAgentLoading(JDK 9+)时,retransformClasses 才可用。但多数生产环境默认关着。
- JDK 8u40+ 和 JDK 9+ 默认允许 retransform,但前提是 agent 是通过
agentmain动态加载的,premain加载的 agent 默认无权调用 - 检查是否真在运行时 attach:用
jps -l看 PID,再用com.sun.tools.attach.VirtualMachine.attach(pid)加载 jar,而不是靠启动参数 - 如果只是想“看类加载”,其实不需要 retransform —— 把
transform的return null改成直接打印,足够定位问题
为什么 agent 里 new 对象会触发无限递归或 ClassCircularityError
因为你的 transformer 在处理 java.util.ArrayList 时,如果内部又调用了 System.out.println,而这个方法底层又依赖 String、StringBuilder 等,它们本身也在被 transformer 拦截 —— 就形成了循环调用。
- 所有日志、异常捕获、对象创建,都必须限定在“已加载且不被 transformer 拦截”的类里,比如用
java.lang.System的静态方法,但避开任何可能触发新类加载的操作 - 安全做法是:只用
System.err.print(它底层不走 java 类库的复杂路径),或把日志写到文件(用FileOutputStream,提前缓存好 fd) - 千万别在
transform里 new 自定义类、调用反射、解析 JSON、打 logback 日志 —— 这些都会触发新类加载,极易崩
真正难的不是写 agent,是控制住自己别在 transformer 里“做事情”。它是个狭窄的通道,不是个应用容器。











