背景

通过监控发现,接口响应慢主要是 P99 耗时高引起的。怀疑与 GC 有关。

负载 单机QPS
高负载 >1000
中负载 500~600
低负载 <200

默认

JDK 8 默认的垃圾回收器是 Parallel GC。即 Young 区采用 Parallel Scavenge,老年代采用 Parallel Old 进行收集。该配置主要特点是吞吐量优先,一般适用于后台任务型服务器。比如批量订单处理、科学计算等对吞吐量敏感的,但对时延不敏感的场景。如果是交互类的,对时延非常敏感,因此,就不适用于这款垃圾回收器。

分代假说

JVM 基于分代假说,提供基于不同代使用不同类型的垃圾回收算法。绝大部分的对象是朝生夕死的,很少有对象经过多轮 GC 仍能存活。如果仍存活,它们晋升到老年代中。但是,分代假说并非银弹,在高并发场景下,会有大量朝生夕死的对象被创建,进而填满整个 Young 区。在监控画面显示就是 Young GC 次数非常频繁。而且,还会引起本应被 Young GC 回收的对象过早晋升,增加 Full GC 的频率。JDk 8 默认使用 Parallel Old,基于标记-整理算法实现,但是它无法与用户线程并行执行,导致服务长时间停顿,可用性下降。

当前主流垃圾回收器

垃圾回收器组合 优点 缺点
Parallel Scavenge
Parallel Old
吞吐量优先,后台任务型服务适合 对时延敏感类应用不友好
ParNew + CMS 经典低停顿收集器,大多数商用、延时敏感的服务在使用
G1 JDK 9 默认收集器。堆内存较大时(6GB 以上)表现出较高吞吐量和低时延特性 堆空间占用较多,
ZGC JDK 11 推出一款低延迟垃圾回收器

调优 ParNew + CMS

调优原则

  1. 元数据区 Metaspace 大小一定要指定。通过 jstat -gc 查看。
  2. Young 区并非越大越好。

    1. Young 区过大,造成 Old 过小,那么年轻代晋升到老年代可能会因为空间不够而频繁触发 Full GC。Full GC 是非常耗时的操作,我们需要极力避免。
    2. Young 区过小,最直接的影响就是 Young GC 频繁被触发。而且,由于 Old 区较大,Full GC 耗时更多。

      压测参数

  3. 不同的垃圾回收器组合形式。

    1. Parallel Scavenge + Parallel Old
    2. ParNew + CMS。
  4. Young 区和 Old 区分配。
  5. 元数据区大小。

    压测结论

  6. ParNew + CMS 的组合远远好于 Parallel Scavenge + Parallel Old

  7. Young 区提升 0.5 倍表现最佳。P99 延时降低 50%,Full GC 累积耗时减少 88%。Young GC 次数减少 23%,Young GC 累积耗时减少 4%。

    还是出现问题

    FUll GC 出现毛刺 耗时过高.webp
    我们需要明确 CMS 收集器何时会触发 Full GC 执行。
    对于 CMS,采用的收集算法是 Mark-Sweep-Compact,GC 种类有两种:

  8. Background GC。一个周期性任务,由 JVM 常驻线程定时扫描老年代的使用率。当使用率超过阈值时触发 Full GC,采用 Mark-Sweep 方式。由于没有 Compact 这种耗时操作,且可以与用户并行执行,所以这一步相对来说耗时较低。GC 日志出现一次 CMS Initial Mark 就意味着发生了一次 Background GC。

  9. Foreground GC(前台 GC)。这种 GC 是真正意义上的 CMS 的 Full GC。采用 Serial Old 或 Parallel Old 进行收集。出现频率较低,一旦出现,往往会造成机器较大的停顿。触发的场景总结如下:
    1. System.gc
    2. jmap -histo:live pid
    3. 元数据区域空间不足
    4. 晋升失败。GC 日志出现:ParNew(promotion failed)
    5. 并发模式失败。GC 日志出现:concurrent mode failure

所以,根据监控信息显示,不难推断,毛刺的出现多半是由于对象晋升失败或并发模式失败而导致 Foreground GC,深层本质是多次 Background GC 造成老年代内存碎片严重,无法找到一块连续的内存存储新晋升的对象。

相关调优参数

  1. Background GC 触发 Full GC 的阈值。由 -XX:CMSInitiatingOccupancyFraction-XX:+UseCMSInitiatingOccupancyOnly 两个参数控制。默认首次为 92%,后续会根据历史情况进行预测,动态调整。我们应设置一个相对合理的值。目标:不使 GC 过于频繁,又可以降低晋升失败或并发模式失败的概率。这就可大大缓存毛刺产生的频率。按经验数据,75%、80% 是折中值,我们可以进行测试。结果是 75% 优于 80%
  2. -XX:+CMSScavengeBeforeRemark 在重新标记前先执行一次新第一代 GC。

参数:

  1. -Xms4096M -Xmx4096M -Xmn1536M
  2. -XX:MetaspaceSize=256M
  3. -XX:MaxMetaspaceSize=256M
  4. -XX:+UseParNewGC
  5. -XX:+UseConcMarkSweepGC
  6. -XX:+CMSScavengeBeforeRemark
  7. -XX:CMSInitiatingOccupancyFraction=75
  8. -XX:+UseCMSInitiatingOccupancyOnly

过程回溯

评估必要性 -> 确定目标 -> 问题分析/方案制定 -> 验证 -> 结果验收 -> 完成