JFR可精准定位对象分配热点,需启用profile配置以开启分配事件采样,在JMC中通过Allocation Profiling视图分析Allocating Method与Object Type,并结合async-profiler验证大对象分配及TLAB外分配场景。

用 JFR 录制分配热点,别只看 GC 日志
GC 频繁只是表象,真正要抓的是“谁在疯狂 new 对象”。JDK 8u262+(OpenJDK 11+ 默认支持)的 JFR 能直接记录对象分配事件,比等 Full GC 再分析堆 dump 快得多、准得多。启动时加参数:-XX:+FlightRecorder -XX:StartFlightRecording=duration=60s,filename=/tmp/alloc.jfr,settings=profile。注意别用 default 配置——它默认关闭分配采样;profile 才开启 jdk.ObjectAllocationInNewTLAB 和 jdk.ObjectAllocationOutsideTLAB 两类事件。
在 JMC 里看 “Allocation Profiling” 视图
用 JDK Mission Control(JMC)打开 alloc.jfr,切到 Allocation Profiling 标签页,重点关注两列:
-
Allocating Method:按分配字节数倒序排,一眼就能看到哪几个方法占了 80% 以上 -
Object Type:确认是不是byte[]、String、HashMap$Node这类高频对象——如果全是char[],大概率是字符串拼接或 JSON 序列化问题
右键某个热点方法 → Open in Editor,能直接跳转到源码行(需配置好源码路径)。注意:JFR 不记录每次分配的完整栈,而是采样;所以看到 “500MB allocated in OrderService.process()”,不代表这方法本身 new 了 500MB,而是它调用链上所有分配都算在它头上。
验证是否真由代码引起:用 async-profiler 抓大对象分配
如果 JFR 显示大量 byte[] 分配,但不确定是哪次反序列化或 IO 导致的,就用 async-profiler 定向抓大对象:
- 先查 GC 日志里有没有
concurrent humongous allocation,确认大对象大小(如allocation request: 1941835784 bytes) - 再执行:
./profiler.sh -e alloc -d 30 -f /tmp/humongous.jfr,它会捕获所有 ≥ 2MB 的分配栈 - 在 JMC 中打开,过滤
jdk.ObjectAllocationInNewTLAB事件,按Size排序,直接定位到TCompactProtocol.readDouble这类可疑调用点
这个步骤能绕过 JFR 的采样偏差,对单次超大分配(比如读一个 2GB 文件进内存)一击必中。
别漏掉 TLAB 外分配和逃逸分析失效场景
即使 ObjectAllocationInNewTLAB 事件很多,也要检查 ObjectAllocationOutsideTLAB 是否同步飙升——这意味着对象没走线程本地缓冲区,直接进 Eden 区,通常因为:
- 对象太大(超过
-XX:TLABSize或 G1 的 region 半宽),被迫走共享 Eden - 方法内对象被方法外引用(逃逸),JIT 关闭了标量替换,导致本可栈上分配的对象硬生生堆上 new 出来
- 用了
Unsafe.allocateInstance()或反射构造,绕过了普通分配路径,JFR 不记录
这类分配往往更危险:不仅耗内存,还加剧 GC 竞争。用 jstat -gc 对比 EC(Eden 容量)和 EU(Eden 使用量),如果 EU 飙升但 OC/OU(Old 区)几乎不动,说明对象生命周期短、全在 Young GC 里回收——这时优化重点不是减少对象数,而是让它们尽量在 TLAB 内分配,或者用对象池复用。











