Skip to content

JVM 垃圾回收调优的主要目标是什么? ​

  • 标签:JVM、Java

🏛 业务架构师 ​

GC 调优的目标不是"GC 越少越好",而是匹配业务 SLA 的三个指标:

  • 低停顿(延迟敏感业务):交易、推荐实时打分——单次 STW 与 P99 强相关。接口 P99 要求 100ms,那 Full GC 停顿 3 秒就是事故,目标是控制停顿上限(G1 的 MaxGCPauseMillis);
  • 高吞吐(批处理业务):报表、离线跑批——总耗时里 GC 占比最小化,偶尔长停顿无所谓,吞吐收集器(Parallel)反而最合适;
  • 低资源占用(成本敏感):容器里多服务混部、Serverless 按量付费——堆越小越省钱,接受更频繁的小 GC。

"GC 频率 vs 单次停顿"的权衡是核心:大堆 = GC 少但单次久;小堆反之。没有全优,只有按 SLA 定价。

我的调优方法论(顺序不能乱):

  1. 先定业务指标:P99 50ms 的接口和 T+1 的批处理,调优方向完全相反;
  2. 看数据不猜:GC 日志(-Xlog:gc*)+ 监控(GC 次数/耗时分布、堆各区趋势)——大多数"GC 问题"其实是内存问题:缓存无限增长、大对象、内存泄漏,调 GC 参数治不了业务代码的病;
  3. 调优动作从小到大:堆大小 → 新生代比例 → 换收集器(Parallel→G1→ZGC 按停顿要求升级)→ 代码层修内存行为;
  4. 验证闭环:改动前后同口径压测对比——没有对比数据的调优是玄学。

一个务实结论:JDK 11+ 默认 G1 + 合理堆大小,能覆盖 90% 业务;真正需要深度调优的是延迟尖刻或内存超大的系统——把调优精力留给值得的场景。

🏗 系统架构师 ​

GC 调优目标我归纳为四条(按优先级):

  1. 控制停顿时间(低延迟优先):核心交易链路 P99 敏感,目标如"单次停顿 < 200ms、日 Full GC = 0"。手段:换低停顿收集器(CMS → G1 → ZGC)、设 -XX:MaxGCPauseMillis(G1 会自适应调整年轻代大小);
  2. 控制 GC 频率与总耗时:GC 吞吐占比 < 5%。手段:堆大小与分代比例调优,减少"过早晋升"(Survivor 太小、MaxTenuringThreshold 不合理导致对象速老);
  3. 合理内存占用:堆不是越大越好——太大单次回收耗时上升(老年代整理慢),且挤压堆外/元空间;目标是"最小堆满足停顿目标",容量成本也省;
  4. 消除异常模式:内存泄漏(老年代锯齿上升)、Full GC 频发、元空间泄漏、堆外泄漏——这些是调不出来只能修的问题,先定位再谈参数。

我的实操顺序:先定业务指标(P99/吞吐)→ GC 日志 + 监控(GCEasy/arthurhome)看现状 → 找主要矛盾(停顿长 or 频率高 or 泄漏)→ 调一个变量观察(改收集器/堆大小/分代)→ 压测回归。没有指标的调优是玄学,一切参数修改都要有前后对比数据;并把 GC 日志作为服务标配(-Xlog:gc*),上线后持续盯 promote 速率与暂停分布。

💻 全栈开发专家 ​

调优前先定目标——三者权衡,不可能全要:

  1. 低停顿(Latency):GC 暂停时间短,接口 P99 稳定——在线服务第一诉求;
  2. 高吞吐(Throughput):GC 时间占比低(如 <1%),批处理/计算型优先;
  3. 低内存占用(Footprint):堆越小越好,容器环境敏感。

调优的本质是给 JVM 输入正确的"预算":多大的堆、多快晋升、用什么收集器换什么指标。

围绕目标的具体抓手:

  • 让短命对象死在新生代(Minor GC 便宜),避免过早晋升导致老年代膨胀引发 Full GC:调大新生代/Survivor、MaxTenuringThreshold 观察;
  • 控制 Full GC 频率为零或极低(G1/ZGC 时代基本可做到);
  • GC 频率和单次时长的健康区间:Minor GC 几毫秒、间隔数秒~数分钟;停顿超过 200ms 的在线服务就该处理。

方法论(我实际的做法):

text
定指标(P99 停顿 < 100ms)→ 开 GC 日志收集现状(频率/时长/晋升速率/大对象)
→ 找主要矛盾(是频繁 Young GC?还是 Old 区满触发 Full?)
→ 单一变量调整(先换收集器再调内存比例)→ 压测对比 → 固化参数

核心认知:GC 调优是最后手段,多数"GC 问题"的病根是代码(一次性查 10 万行、无界缓存、大对象循环创建)——先修代码再动参数。