GC 日志是 JVM 调优和线上排查的第一手证据。接口变慢时,不能只看业务日志里的耗时,还要判断是否发生了 Stop The World、Full GC、老年代空间不足、晋升失败或大对象分配。供应链系统在大促、仓库波次下发、订单集中履约时对象创建速度很快,GC 日志能帮助我们把问题从“感觉慢”变成可量化的事实。
整体流程图
常见垃圾收集器
不同 JDK 版本和业务场景会使用不同收集器:
- Serial GC:单线程收集,适合小内存客户端程序,不适合高并发服务。
- Parallel GC:吞吐优先,适合批处理任务,例如夜间库存重算、历史订单归档。
- CMS:低停顿老年代收集器,JDK 9 后被标记废弃,JDK 14 移除。
- G1:面向服务端低停顿场景,JDK 9 以后成为默认收集器,适合大多数订单、库存、仓储服务。
- ZGC:低停顿收集器,适合大堆和低延迟服务,具体使用要结合 JDK 版本和生产验证。
如果没有特殊原因,现代 Spring Boot 服务通常优先使用 G1。它把堆划分为多个 Region,通过预测停顿时间选择回收集合,目标是在可控停顿下获得稳定吞吐。
仓储高峰期 Demo
假设 WMS 在晚上 8 点下发波次任务,订单服务要把 30 万个待出库订单按仓库、承运商、优先级分组:
1 | public class WaveDispatchService { |
这段代码业务上清晰,但在高峰期会创建大量临时对象:分组 key、Map 节点、List、Stream 中间对象、WaveGroup。如果堆空间偏小或对象晋升过快,GC 停顿会明显增加。
如何打开 GC 日志
JDK 8 常用参数:
1 | -XX:+PrintGCDetails \ |
JDK 9 及以后推荐使用统一日志:
1 | -Xlog:gc*,safepoint:file=/data/logs/order-service/gc.log:time,uptime,level,tags:filecount=10,filesize=100M |
线上服务建议默认开启 GC 日志。日志文件滚动要配置好,否则长时间运行可能撑爆磁盘。
GC 日志重点看什么
分析 GC 日志时,不要只看有没有 GC,要看四类指标:
- 频率:Young GC、Mixed GC、Full GC 多久发生一次。
- 停顿时间:每次暂停多少毫秒,P95/P99 是否影响接口 SLA。
- 回收效果:GC 前后堆、老年代、元空间占用下降多少。
- 触发原因:Allocation Failure、Metadata GC Threshold、Humongous Allocation、System.gc() 等。
一段简化后的日志可能类似:
1 | [2023-06-18T20:01:12.345+0800][info][gc] GC(42) Pause Young (Normal) 512M->180M(1024M) 35.7ms |
这说明 JVM 正在进行新生代回收,并启动并发标记。如果后面频繁出现 Full GC,且每次回收后老年代下降不明显,就要怀疑长生命周期对象过多或内存泄漏。
常见判断结论
GC 日志可以帮助形成明确结论:
- Young GC 频繁但停顿短:对象创建速度快,可能需要优化批处理对象分配,或者适当增大堆。
- Full GC 频繁且回收效果差:老年代长期占满,优先查缓存、静态集合、批量任务和大对象。
- 元空间触发 GC:检查动态代理、脚本引擎、热部署和类加载器泄漏。
- 大对象触发回收:检查一次性大数组、大 JSON、大 Excel 导出。
- 明确出现
System.gc():检查代码、第三方库或运维脚本是否主动触发 Full GC。
优化仓储波次的代码思路
对于前面的分组逻辑,可以从业务和代码两侧降压:
1 | public void dispatchWaveByPage(String waveNo) { |
核心不是手动调用 GC,而是控制对象峰值:分页查询、分批落库、避免超大集合、减少无意义字符串拼接。GC 调优优先解决对象生命周期和分配速率,再调整 JVM 参数。
GC 日志的价值在于把性能问题证据化。对于供应链系统,订单高峰、仓储波次、库存同步都可能制造对象洪峰,只有把日志、业务峰值和代码路径结合起来看,才能得出可靠结论。