记一次 CMS GC导致 FULL GC 时间开销很大的排查

Posted Agly_Charlie

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了记一次 CMS GC导致 FULL GC 时间开销很大的排查相关的知识,希望对你有一定的参考价值。

背景

  1. 服务接入注册中心后,就会有实例健康检查,通过ip+port的方式访问接口,检查实例是否健康,某日有个实例出现了告警。检查发现是当时接口超时异常了,触发告警。
  2. 短暂的时间后,服务又正常恢复了,接口正常响应。观察日志没有异常问题。

定位分析过程

  1. 这个健康接口是个简单的返回,没有经过DB、缓存等。所以超时问题出现在服务本身。
  2. 查看CAT、机器基础监控,检查内存,CPU,GC等监控,发现FULL GC在这个时间点有个很长的耗时,环比其他时间都没有这个现象。
  3. 定位是GC 导致 时间很长,服务暂停,导致接口超时。

第一次尝试解决方案

  1. 分析:服务的JVM参数是 -xms2g -xmx5g
  2. 考虑到FULL GC时间长,那应该是老年代需要回收的垃圾太多了,导致整个GC耗时长了,所以调整-xmx4g,缩小JVM的大小,老年代尽快完成FULl GC.
  3. 效果不明显,还是会出现这个FULL GC耗时长,接口超时告警. 进一步分析JVM,服务使用的是CMS GC收集器,考虑这里有个STW的阶段可能是原因。

CMS GC收集器分析

了解CMS收集原理

CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,多线程标记清除算法,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的。整个过程分为四个步骤:

  1. 初始标记: 暂停所有的其他线程,并标记一下 GC Roots 能直接关联的对象,速度很快 ;
  2. 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
  3. 重新标记: 需要暂停所有的工作线程,为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。
  4. 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。

优缺点分析总结

它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面的缺点:

  1. 无法处理浮动垃圾;
  2. 回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

分析根因

  1. 项目中大量使用了本地缓存,使用 guava cache 缓存了很多数据。
  2. 对于CMS垃圾收集器,如果是大量本地缓存应用的话,重新标记阶段这个耗时就会较长,因为在并发阶段很容易有很多新对象进入缓存,从而重新标记阶段扫描很耗时,并且重新标记阶段是STW,暂停所有的工作线程。
  3. 当刚好进行并发阶段,同时缓存正在重新加载刷新的时候,就好会出现这个问题。

解决方案

  1. 缓存设计上,减少不必要的缓存,如一些冷数据不再缓存。
  2. 开启-XX:CMSScavengeBeforeRemark,在重新标记阶段前进行一次YGC,有利于减少 Young Gen 对 Old Gen 的无效引用,从而减少重新标记阶段扫描GC Roots的开销。
  3. 尝试使用G1垃圾收集器,通过-XX:MaxGCPauseMillis设置最大停顿时间,提高服务可用性。
  4. 考虑团队用的JVM 收集器都是CMS,本次优化是减少缓存的使用,同时开启 -XX:CMSScavengeBeforeRemark

服务使用大量本地缓存,CMS GC收集器重新标记阶段导致STW时间很长。

以上是关于记一次 CMS GC导致 FULL GC 时间开销很大的排查的主要内容,如果未能解决你的问题,请参考以下文章

记一次 CMS GC导致 FULL GC 时间开销很大的排查

记一次 CMS GC导致 FULL GC 时间开销很大的排查

记一次诡异的频繁Full GC

JVM初探- 使用堆外内存减少Full GC

记一次服务Full GC背后的内存泄漏问题,真是匪夷所思

深入理解Major GC, Full GC, CMS