直接看GC行为和对象生成源头,不用猜:盯住Eden区填满速度和YGC频率,EU长期98%~99%且YGC每秒超5~10次即锁定年轻代分配风暴;结合GC日志中“Allocation Failure”及DefNew回收前后对比,确认大量短命对象;堆转储用MAT定位String/StringBuilder高频小对象,再通过javap反编译验证循环内隐式new StringBuilder。

直接看 GC 行为和对象生成源头,不用猜。Profiler 的作用不是“找慢代码”,而是确认“谁在疯狂造垃圾”。
盯住 Eden 区填满速度和 YGC 频率
这是最快速、最可靠的起点。频繁 GC 不是症状,而是现象;Eden 持续打满才是根因。
- 用 jstat -gc <pid> 1000 每秒刷新:重点关注 S0U、S1U、EU(Eden 使用量) —— 如果 EU 长期稳定在 98%~99%,且 YGC 次数每秒超过 5~10 次,基本锁定年轻代分配风暴
- 配合 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps 启动 JVM:日志里会反复出现类似
[GC (Allocation Failure) [DefNew: 65536K->1234K(73728K), 0.0152345 secs],说明每次 GC 前 Eden 都几乎耗尽,GC 后存活对象极少(→ 大量短命对象)
堆转储里抓高频小对象
GC 频繁但老年代没爆?说明问题在“产得快、死得早”,目标就是找出这些“一闪而过”的对象。
- 触发一次 Full GC 或手动 jmap -dump:format=b,file=heap.hprof <pid> 导出堆快照
- 用 Eclipse MAT 打开,看 Leak Suspects 和 Top Consumers:如果 java.lang.String 和 java.lang.StringBuilder 占 Retained Heap 前两位,且实例数达数十万甚至百万级,每个对象 retained heap 很小(几 KB 以内),就高度指向循环内 +=
- 点开其中一个 StringBuilder 实例 → 查看其 outgoing references → 往上追溯到持有它的局部变量或方法栈帧,通常能直接定位到某段 for/while 循环代码
字节码反编译确认隐式 new
编译器是否真在循环里塞了 new StringBuilder?不看字节码,永远是推测。
立即学习“Java免费学习笔记(深入)”;
- 用 javap -c YourClass 反编译问题类:搜索循环对应的方法,在字节码中找 new java/lang/StringBuilder 和 invokevirtual ... toString() 指令对——只要它们出现在循环体内部(比如在 goto 或 if_icmpge 控制流包围中),就坐实了问题
- 注意:静态拼接如
"a" + "b"会被优化掉,不会出现 new;但含变量的str += "x"或s1 + s2必然生成这两条指令
Profiler 工具辅助验证(非必需但直观)
像 JProfiler、VisualVM 或 Async Profiler 这类工具,能帮你跳过命令行,把上述线索可视化。
- 开启 Allocation Recording(不是 CPU Profiling):运行几分钟后停止,按“Allocated Objects”排序,排第一的如果是 StringBuilder 或 String,再按 “Call Tree” 展开,直接看到哪行代码调用了 new
- 注意过滤掉日志框架、JSON 序列化等第三方库的正常分配,聚焦在你自己的业务方法里——尤其关注那些被高频调用、参数含字符串、内部有循环的 method
- Async Profiler 的
profiler.sh -e alloc更轻量,输出火焰图按分配量着色,热点函数一目了然


















