深入CMS的六大阶段

2023-07-30 jvm

由于历代垃圾回收器都是串行或独占或回收的,例如前面3个年轻代回收器 + 2个老年代回收器,都是必须停止工作线程后,GC线程才开始垃圾清除。在这样的大背景下,于2002年JDK1.4.2发布CMS,它是哪个时代第一次实现并发收集器(相对来说),即实现了让垃圾收集线程与用户线程同时工作。

CMS的特色就是停顿时间短(低延迟), 停顿时间越短就越适合用户交互的程序,越能提升用户体验。在G1收集器面世之前,CMS基本都是JVM的标配,甚至是现在市面都是很多系统在使用CMS。

# 1.初始标记(InitialMarking)

这是一个STW过程,但是业务线程暂停时间很短,因为仅标记GCRoots对象,主要分两步

  1. 标记GC Roots可达的老年代对象;
  2. 遍历GC Roots下的新生代对象能够可达的老年代对象;

CMS初始标记

# 2.并发标记(Marking)

该阶段GC线程和应用线程并发执行,遍历初始标记阶段标记出来的存活对象,并发标记阶段因为与用户线程同时运行,不用STW;不过这个过程应用线程在运行,可能Young GC也会发生,会发生以下的情况:

  1. 新生代对象晋升到老年代
  2. 在老年代分配对象(surivor空间不足,直接分配对象到老年代)
  3. 新老年代对象的引用发生变化。

解决方法是JVM设计了一个cardtable来记录并发阶段老年代对象变更后的存储,会对上述对象变化情况所在的Card标记为Dirty,后续只需要扫描这些Dirty Card的对象,避免扫描整个老年代,并发标记阶段只负责将引用发生变化的Card标记为Dirty状态,不负责处理。

如图所示,橙色的会遍历其子节点如果存活则记录否则交给最后的GC处理,绿色的是被标记为Dirty的,下一步处理。

CMS并发标记

# 3.并发预处理

此阶段不会STW,前一阶段已经说明了不能标记出老年代所有的存活对象,是因为标记是与用户线程并发执行的,在此期间会改变一些对象引用,而并发预处理这个阶段是用来处理前一个阶段变化的对象,它会扫描标记为Dirty的Card,将其也加入存活对象的集合中;可使用 -XX:-CMSPrecleaningEnabled 来关闭,默认开启,其目的是为了让最终/重新标记阶段的STW时间尽可能短。

# 4.可终止的并发预处理

此阶段不会STW,触发的前提是eden区的内存使用大于参数CMSScheduleRemarkEdenSizeThreshold默认是2M。如果eden区对象太少就没必要执行,直接跳过进行下一阶段《重新标记》,这一阶段主要做两件事:

  1. 处理surivivor区的对象,标记可达的老年代对象
  2. 和上一阶段《并发预处理》一样,处理cardtable对象

设计该阶段的目的就是为了给下一阶段《重新标记阶段》降低压力,保障STW暂停时间最短,同时该阶段还设置了3个中断条件

  1. 设置最多循环的次数CMSMaxAbortablePrecleanLoops,默认是0表示没有循环次数的限制。
  2. 如果这个阶段的时间达到了阈值CMSMaxAbortablePrecleanTime,默认是5s,会退出循环。
  3. 如果Eden区的内存使用率达到了阈值CMSScheduleRemarkEdenPenetration,默认50%,会退出循环。

# 5.重新标记

这个阶段会导致第二次STW,《预清理阶段》和《可中断的预清理》都是为重新标志阶段做准备,由于重新标志阶段会发生(STW),所以要保证尽肯能的停顿时间段,不然就会影响应用程序的用户体验,这一阶段主要干3件事:

  1. 扫描整个年轻代:由于老年代中的对象可能会引入已经不可达的新生代,如果不扫描新生代,老年代中的这些对象将无法被释放,容易引发内存泄漏。
  2. 扫描GC Roots:扫描的目的是如果老年代中对象A可以找到对象B,但是对象B已经被A释放掉了,B就是死亡对象,那就需要重新扫描标记B为死亡对象。
  3. 扫描cardtable:和《并发预处理》阶段一样扫描cardtable,把dirty的card找出来,将card中的对象重新GCRoots标记可直接或间接到达的对象。

# 扫描整个年轻代时间长该如何优化?

当大量引用老年代的新生代对象死亡时,耗时较长的时候,可以加入参数-XX:+CMSScavengeBeforeRemark,在重新标记之前先执行一次YoungGC,回收掉年轻带中无用的对象,并将对象放入幸存区(surivivor)或晋升到老年代,这样再进行年轻带扫描时,只需要扫描幸存区的对象即可,一般幸存区(surivivor)非常小,这大大减少了扫描时间。

# 6.并发清理

通过上述的5各阶段的标记,老年代所有可存活的对象已经被标记,现在要垃圾回收器回收不需要用的对象。这个阶段主要是清理那些没有被标记的对象并释放空间。

由于CMS并发清理阶段用户线程还在运行,CMS无法在当次收集中处理掉他们,只能等到下一轮GC再清理。这部分垃圾称为“浮动垃圾”。

# CMS为什么会出现内存碎片?如何解决?

经历了并发清除阶段后垃圾对象都被清除了,剩下的内存碎片,碎片化导致内存空间不连续,内存空间白白被占用但没数据,因为不连续的小空间,无法容纳下一个对象。

为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullIGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,

空间碎片问题没有了,但停顿时间不得不变长,虚拟机设计者还提供了另外-个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,默认值为0,表示每次进入Full GC时都进行碎片整理,

CMSFullGCsBeforeCompaction配置为10,就会让上面说的第一个条件 变成每隔10次真正的full GC才做一次压缩,(而不是每10次CMS并发CC就做一次压缩, 目前VM里没有这样的参数)。这会使full GC更少做压缩,也就更容易使CMS的old gen受碎片化问题的困扰。

上次更新: 5 个月前