应先观察分配行为而非等Full GC报警;用jstat确认新生代分配过快,jmap -histo定位高频分配对象,结合代码排查字符串拼接、日志序列化等高分配场景,再用JFR等工具验证分配热点。

直接看对象分配行为本身,而不是等 Full GC 报警才动手。频繁 GC 的根源往往藏在“谁在疯狂 new”和“谁没被及时回收”里。
第一步:确认是不是真因分配过大引发的 GC 压力
用 jstat -gc <pid> 1000 10 实时观察,重点盯三列:
- YGC 次数每秒 ≥2 次,且 YGCT(总耗时)同步明显上升 → 新生代对象生成过快
- Eden 区每轮 GC 后几乎立刻打满 → 分配速率远超回收能力
- S0/S1 使用率长期低于 10% 或反复清零 → 对象存活时间极短,但量太大,Survivor 区根本“装不下”,大量对象直接晋升老年代
第二步:定位高频分配的大对象或集合
别急着 dump 全堆,先快速筛出嫌疑对象:
- 执行 jmap -histo:live <pid> | head -20,关注前三名:[B(byte[])、java.util.ArrayList、java.util.HashMap$Node —— 它们占总实例数 60% 以上,基本就是分配大户
- 特别留意 业务 POJO 类 实例数是否达数十万甚至百万级,且随请求量线性增长 → 很可能在循环中 new、或缓存未设上限
- 如果 [B 排第一且单个实例 >1MB,检查是否有大文件读取、Base64 解码、JSON 反序列化未流式处理等操作
第三步:结合代码查典型高分配场景
这些写法在线上极易造成“秒级千次 new”:
立即学习“Java免费学习笔记(深入)”;
-
字符串拼接滥用:在 for 循环里用
str += "xxx",每次触发 new StringBuilder + toString,生成大量中间 char[] -
日志参数未控制:如
log.info("user={}, detail={}", user, JSON.toJSONString(detail)),detail 是大对象时,JSON 序列化会分配巨量 byte[] 和 String -
Stream 处理无限制:比如
list.parallelStream().map(...).collect(Collectors.toList()),原始 list 百万级,直接 OOM 前兆 - DTO 转换不复用:MapStruct 每次都 new 新对象,而非用 @Mapping(target = "xxx", expression = "java(...)") 控制字段级赋值
第四步:用工具验证分配热点(可选但高效)
对问题接口做轻量级采样:
- JDK 自带 jcmd <pid> VM.native_memory summary 看整体内存分布趋势
- 加 JVM 参数 -XX:+UnlockDiagnosticVMOptions -XX:+PrintAllocationPressure(JDK 17+),可输出高压力分配栈
- 用 JFR(Java Flight Recorder)录制 60 秒:jcmd <pid> VM.start_flight_recording duration=60s filename=alloc.jfr,然后用 JDK Mission Control 打开,筛选 “Allocation Requiring GC” 事件,直接看到哪行代码在疯狂分配


















