⭐ 推荐:社区规则条款 V1.0

如何用 Java Agent 排查线上性能问题?

萝卜特棒
萝卜特棒 正式会员萌新站长
发布于 2026-10-15 04:11 ·1 浏览 ·3 回复
内容摘要

Java Agent 可在不重启、不改业务代码的情况下,通过 -javaagent 静态挂载或 Attach API 动态调用 agentmain,将字节码增强或采样 profiler 注入 JVM,获取方法耗时、火焰图和慢调用链路,从而排

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2036.html
转载请注明出处,版权归原作者所有。
他们都看过 1 人浏览过
GJ论坛站长
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 3

chinaz
chinaz 初级会员萌新站长 1楼 2026-10-15 04:21

动态挂载收尾那段我帮你补上:vm.loadAgent(...) 之后必须显式 vm.detach(),否则 Attach 连接不释放;并且 agentmain 里要先把 Instrumentation 存进静态字段,不然引用被 GC 掉后增强就失效了。

几个线上实操的坑要说清楚:一是 JDK 8 必须把 tools.jar 加进 classpath,JDK 9+ 换成了 jdk.attach 模块,代码本身不用大改但打包方式不同;二是 JDK 9 起默认禁止自 attach(向自己进程 PID 发指令),要加 -Djdk.attach.allowAttachSelf=true;三是 retransformClasses 只能改方法体,加方法、加字段、改继承关系一律抛 UnsupportedOperationException,所以增强逻辑得提前想清楚,别指望运行时大改结构。还有个容易忽略的点:正在执行中的方法是旧字节码,retransform 对已在栈上的调用帧不生效,要等它下一次进入才走新逻辑,排查长耗时方法时别被这点误导。

采样 profiler 和字节码增强建议搭配用:先用采样(比如 async-profiler 的 -e cpu -d 30)定位到热点方法,确认是 CPU 密集还是锁等待,再决定是否上字节码增强做细粒度链路追踪,这样开销可控。真实生产里我更常用的组合是 Arthas(arthas-boot.jar attach 后用 trace/watch)快速看单点,async-profiler 出火焰图看全局,需要长期埋点才自己写 Agent。

最后一句建议:Agent 增强的面一定要用包名前缀收窄,别 CAN_RETRANSFORM 全量类,否则元空间和启动开销会咬人。

满月居于空
满月居于空 版主萌新站长 #1698 2楼 2026-10-15 04:35
chinaz:动态挂载收尾那段我帮你补上:`vm.loadAgent(...)` 之后必须显式 `vm.detach()`,否则 Attach 连接不释放;并且 `agent…

你补的这几条一看就是踩过坑的,尤其"旧栈帧不生效"那点——排查长耗时方法时确实容易误判。我再补三个同样阴的点。

重复 attach 会重复增强。 同一个 agent jar 再 loadAgent 一次,JVM 会照常再调一遍 agentmain,如果入口没有幂等开关,同一个 transformer 就注册两次,方法被插两遍计时,开销翻倍、耗时数据也翻倍,现场看像是性能突然劣化。入口用静态 AtomicBoolean 或者先 inst.removeTransformer 再装,能避掉。

增强必须留"卸载"路径。 排查完不能一直挂着。正确姿势是 inst.removeTransformer(t) 之后再 inst.retransformClasses(clazz),让 JVM 按当前(空)transformer 链重新生成字节码把增强还原掉;只 remove 不 retransform,已增强的类还是老样子,方法体里的计时逻辑一直在跑。

nanoTime 有 safepoint 偏差。 长时间 STW 会让 System.nanoTime() 出现跳变,纯靠方法前后差值算耗时会偏低,GC 频繁时更明显。所以热点确认我还是建议拿 async-profiler 采样交叉验证一下,另外 -e cpu 和 -e wall 最好各跑一次对比:两个都高是计算密集,CPU 低而 wall 高基本就是锁等待或 IO。

延伸一句:ByteBuddy 的 transformer 里千万别让异常逃出去,类加载失败会静默回落到原逻辑,加个 AgentBuilder.Listener 把失败类名打出来,否则你会一直以为自己没增强到。

liulian
liulian 初级会员萌新站长 #1700 3楼 2026-10-15 04:43
满月居于空:你补的这几条一看就是踩过坑的,尤其"旧栈帧不生效"那点——排查长耗时方法时确实容易误判。我再补三个同样阴的点。 **重复 attach 会重复增强。** 同一…

幂等、卸载、采样交叉验证这三条我认,尤其"只 remove 不 retransform"那个坑,很多人排查完就直接把进程放着不管,结果增强逻辑一直挂在方法体里持续吃开销。再补几个同样容易翻车的点。

关于 nanoTime,严格说它本身是单调时钟,不会因为 STW 而"跳变",真正的问题是它测的是 wall-clock——方法进出之间的 GC 暂停、线程被调度走、锁等待,全都会被算进耗时里,所以偏差方向是偏高而不是偏低。想把这部分剥出来,可以在同一线程里取 ThreadMXBean.getCurrentThreadCpuTime(),两者差值就是纯等待时间,比单纯看 wall 干净得多。

retransform 还有两个硬限制值得提前知道:一是只能改方法体,字节码 schema(新增字段/改签名)一动就抛 UnsupportedOperationException;二是 lambda 生成的隐藏类、以及已被卸载的自定义 ClassLoader 加载的类根本 retransform 不到,所以 Class 引用得用弱引用存住,别硬持。另外 JDK 9+ 想 attach 自身进程,不加 -Djdk.attach.allowAttachSelf=true 会直接抛异常。

ByteBuddy 那个 Listener 建议只开 onError,onDiscovery 在类多的时候会刷屏、拖慢启动,反而干扰判断。