GC 与延迟

垃圾回收会造成周期性停顿并恶化尾延迟:通过堆大小、代际策略与低延迟收集器选择控制停顿,用 GC 日志量化频率与时长再调参。

GC 怎么会拖慢尾延迟

垃圾回收会造成周期性停顿:回收期间应用线程被暂停,单次停顿可能只有几毫秒,但在高并发下会把若干个正在处理的请求一起推后,于是在延迟分布上表现为 P99 明显高于 P50。调参的目标不是消除停顿,而是把停顿的频率与时长控制在延迟目标允许的范围内。有效的前提是先量化:打开 GC 日志,把停顿时间线与 P99 曲线对齐,再决定是调参数、换收集器还是修分配行为。

基本信息

观测GC 日志记录停顿次数与时长;对照 P99 时间线
手段控制堆增长速度、选择低延迟收集器、减少临时对象分配
验证调参前后对比停顿分布与吞吐,避免只盯单次停顿
适用语言JVM(G1 为 JDK 9 起的默认收集器,低延迟场景可选 ZGC)、Go 等有托管堆的运行时

典型使用场景

  • 接口尾延迟偏高:P99 与 P50 差距大且呈周期性尖峰,怀疑与 GC 有关
  • 内存扩容后仍频繁回收:需要区分是堆不足还是分配速率过高
  • 服务升级后延迟劣化:对比升级前后的停顿分布确认影响
  • 高吞吐批处理:用吞吐优先的配置换取更少的回收次数
  • 低延迟交易类服务:为停顿设置明确上限并选择相应收集器

排查顺序

排查顺序
1. 打开 GC 日志(JVM 示例:-Xlog:gc*:file=gc.log:time,uptime)
2. 统计停顿次数、时长分布与回收前后的堆占用
3. 与 P99 时间线对齐:尖峰与停顿是否重合
4. 检查分配速率与对象存活时间,排除内存泄漏与对象风暴
5. 一次只改一个参数,相同负载复测
# 只调参不查分配行为,往往是白忙一场

现象与可能原因

现象可能原因方向
Full GC 频繁内存泄漏、堆过小或缓存无上限先查泄漏与缓存增长,再考虑扩容
停顿长但次数少堆较大且回收器停顿与堆大小相关评估低延迟收集器或调整区域大小
Young GC 频繁但很快分配速率高、临时对象多减少对象分配、调整新生代比例
调参后 P99 无改善瓶颈不在 GC回到延迟分解,逐段定位真正的耗时来源

操作要点

  • 先看分配速率与对象存活时间,排除内存泄漏与对象风暴
  • 整理 GC 日志:停顿次数、时长与回收效果,与 P99 时间线对齐
  • 设定目标:如停顿 P99 上限与 Full GC 频率目标,作为调参的验收条件
  • 选择收集器并调参(堆大小、区域大小、暂停目标),一次只改一个变量
  • 参数固化进部署配置并纳入监控,防止升级后配置丢失
GC 调优的前提是分配行为健康:频繁 Full GC 往往指向内存泄漏或对象风暴,此时调参数只是在拖延问题暴露的时间。
把容器内存上限设成与堆上限相等是常见配置错误:堆之外还有元数据、线程栈与直接内存,会触发容器被 OOM 终止,堆与容器上限之间要留出空间。

易错点

  • 内存泄漏引发的 Full GC 被当作参数问题调优,问题在扩容后被掩盖一段时间
  • 只看平均停顿,忽略影响 P99 的长时间停顿,用户体验与指标表现脱节
  • 堆上限与容器内存上限设成相等,堆外内存占用导致进程被 OOM 终止
  • 一次性修改多个参数,无法判断哪项改动起了作用,复现与回滚都困难
  • 调参结果未固化到部署配置,编排重建或升级后回到默认值,延迟再次劣化

数据来源与参考

  • 《深入理解 Java 虚拟机》(周志明)
  • 《性能之巅(第 2 版)》(Gregg)
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌