高频大数组直接分配至老年代导致“边清边满”和频繁GC,需通过GC日志识别Humongous allocation等特征,结合堆快照与async-profiler定位源头,优先用-XX:PretenureSizeThreshold、ThreadLocal复用及流式API修复。

高频产生大量短命大数组会绕过 Eden 区,直接在老年代分配,造成老年代“边清边满”,引发频繁 Full GC 或 Mixed GC,日志中表现为吞吐量指标(如 GC 吞吐量、应用运行时间占比)断崖式下跌。这不是对象泄漏,而是分配路径错配导致的回收效率坍塌。
看 GC 日志识别大数组分配特征
启用完整 GC 日志(-Xlog:gc*,gc+heap=debug:file=gc.log:time,uptime,level,tags),重点抓三类信号:
- 日志中反复出现 Humongous allocation —— 表明对象已触发 G1 大对象逻辑,直入老年代
- Young GC 后老年代使用量(OG)不降反升,甚至单次增长超 50MB
- Full GC 或 Mixed GC 频率陡增,但每次回收的老年代空间极小(如仅几 MB),说明新对象持续灌入、旧对象尚未死亡
- GC Cause 显示为 Allocation Failure 占比极高,且与业务请求节奏强相关(如每处理 100 条记录就触发一次 Mixed GC)
用堆快照定位大数组源头
执行 jmap -histo:live
- [B(byte[])、[C(char[]) 实例中是否存在单个 >1MB 的对象;若前 5 名全是 [B 且总和占堆 30% 以上,基本可定性
- 这些大数组的 Retained Heap 是否远高于自身大小——说明它们被某容器或缓存强引用,但实际生命周期极短
- 右键查看 “Merge Shortest Paths to GC Roots”,确认调用栈是否集中在报表导出、Excel 解析、批量 JSON 序列化等接口
结合线程与分配追踪交叉验证
运行时用 async-profiler 捕获分配热点:
- 执行:./profiler.sh -e alloc -d 30 -f alloc.html
,聚焦 30 秒内高频分配的 [B / [C 类型 - 在火焰图中下钻,看分配是否密集落在 ForkJoinPool 工作线程、Netty EventLoop 或 HTTP 处理线程中
- 对比相同接口压测前后:开启 -XX:+FlightRecorder -XX:StartFlightRecording=duration=60s,用 JFR 查 jdk.ObjectAllocationInNewTLAB 事件,按 Class 过滤 [B,再按 Stack Trace 分组,排第一的栈帧就是根因位置
轻量修复与效果验证
不改业务逻辑,优先拦截分配洪峰:
- 加 JVM 参数 -XX:PretenureSizeThreshold=1048576(1MB),让部分中等数组回归年轻代,避免过早污染老年代
- 对已知高危方法(如 new byte[2 * 1024 * 1024])改用 ThreadLocal
复用缓冲区,注意 reset 清零 - JSON/Excel 批量处理改用流式 API(如 Jackson Streaming API、Apache POI SXSSF),杜绝一次性加载全量数据
- 压测验证:修复后观察 GC 日志中 Humongous allocation 次数下降 >90%,Mixed GC 频率回落至每分钟 ≤1 次,吞吐量恢复至 95%+(通过 GC 日志中 application time / (application time + gc time) 计算)

















