Skip to content

如何对 Java 的垃圾回收进行调优? ​

  • 标签:JVM、Java

🏛 业务架构师 ​

GC 调优的方法论我概括为**"目标先行、数据驱动、小步验证"**:

第一步:定目标(业务指标翻译成 GC 指标)

  • 接口 P99 200ms → 单次 GC 停顿需远小于此(如 <50ms);
  • 吞吐型任务 → GC 时间占比 <5%;
  • 目标不同,方向相反(大堆低频 vs 小堆低停顿)——没有目标的调优是撒网祈祷。

第二步:采数据

  • GC 日志(-Xlog:gc*)分析停顿分布、频率、回收效果;
  • 堆趋势:老年代增长速率(斜率异常 = 泄漏或缓存失控)、对象画像(jmap histo 找大头);
  • 关联业务指标:GC 停顿与接口毛刺的时间点对齐验证因果。

第三步:按优先级动手

  1. 多数"GC 问题"是内存问题:修代码(缓存上限、大对象复用、减少临时对象)比调参数收益大——调参治标,治本在业务代码的内存行为;
  2. 堆大小与分代比例(Xms=Xmx 防抖动、新生代适配对象晋升率);
  3. 换收集器:Parallel(吞吐)→ G1(均衡,默认即够)→ ZGC(亚毫秒停顿,延迟尖刻业务如实时风控);
  4. 只在证据明确时动 JVM 细节参数——每调一个参数都要能回答"预期改善什么指标、依据是什么"。

第四步:验证闭环:同负载压测前后对比 + 灰度观察线上——没有对比数据的调优改动禁止上线。

务实结论:90% 的应用默认 G1 + 合理堆 + 不写内存失控的代码 = 不需要调优。架构师更该投入的是内存治理规范(缓存必设上限、大集合分页)——把 GC 问题消灭在设计期。

🏗 系统架构师 ​

GC 调优我按"先定目标 → 看数据 → 改关键变量 → 验证固化"的闭环讲:

1. 定目标:低延迟(P99 < 200ms、无 Full GC)还是高吞吐(批处理)?目标决定收集器选择——这是我调优的第一决策。

2. 采集数据:GC 日志(-Xlog:gc*:file=gc.log)+ APM/监控(堆使用曲线、promote 速率、停顿分布)。看三类症状:频繁 Young GC(新生代太小/分配速率高)、对象过早晋升(Survivor 不足)、Old 持续上涨(泄漏 or 缓存滥用)。

3. 关键调优动作:

  • 选对收集器:JDK8 默认 Parallel(吞吐型,停顿大);低延迟服务换 G1(大堆 4~32GB、可控停顿)或 ZGC(TB 级堆、亚毫秒停顿,JDK15+ 生产可用);
  • 堆大小:容器里 -XX:MaxRAMPercentage=70 防止被 OOMKilled,同时预留堆外/元空间;堆不是越大越好,太大单次停顿上升;
  • 分代比例:G1 下让 Region 大小、MaxGCPauseMillis 自治,少手动干预;Parallel 下按"平均请求存活对象量"倒推 Survivor;
  • 治本类问题:缓存无界(换 Caffeine 有界)、大对象直接进老年代(PretenureSizeThreshold 或代码拆分)、Full GC 由元空间/调用 System.gc() 引起的分别处理(-XX:+DisableExplicitGC 注意堆外内存回收依赖,用 ExplicitGCInvokesConcurrent 替代)。

4. 验证:每次只改一个参数,压测对比停顿分布与吞吐,参数进配置管理与发布流程。

一句话:GC 调优 20% 靠参数,80% 靠定位内存分配行为,多数"GC 问题"最后改的是代码不是 JVM 参数。

💻 全栈开发专家 ​

我的调优流程是**"先诊断、后动刀、必验证"**,绝不上来就改参数:

第一步:收集数据

text
# JVM 参数(8+ 统一日志)
-Xlog:gc*,gc+heap=debug:file=gc.log:time,uptime:filecount=5,filesize=50m
# 运行时观测
jstat -gcutil <pid> 1000     # 各区使用率 + YGC/FGC 次数与耗时

GC 日志丢 GCEasy/GCViewer 分析:GC 频率、单次时长、晋升量、大对象分配。

第二步:定位问题模式

  • 频繁 Young GC:新生代太小 / 短命对象太多(代码问题:循环内建对象);
  • Young GC 后 survivor 装不下大量晋升 → 老年代涨 → Full GC:Survivor 太小或 MaxTenuringThreshold 不合理;
  • Old 区持续增长回收不掉:内存泄漏(dump + MAT)或缓存无界;
  • G1 下 Humongous 对象:单对象 > region/2 直接进老年代(一次性拉大结果集的典型)。

第三步:动刀(单一变量原则)

text
-Xms = -Xmx(防堆抖动)      -Xmn 或 G1 用 -XX:MaxGCPauseMillis=200
8G 以下堆:ParallelGC;8G+ 在线服务:G1;超大堆低延迟:ZGC

第四步:压测验证:对比 P99、GC 次数/停顿、吞吐,指标不达标回退。

我修过的真实案例:定时任务每分钟 new 一个 200MB 的报表对象 → G1 频繁 Humongous 分配 → Full GC。改成流式分页处理 + 复用 buffer,Full GC 归零——GC 调优七分靠改代码,三分靠调参数。