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 终止,堆与容器上限之间要留出空间。