JVM基础系列:CMS垃圾回收器

  CMS是老年代垃圾回收器,在回收过程中可以与用户线程,它可以与Serial回收器和Parallel New回收器搭配使用,Java9之后默认年轻代使用Parallel New回收器,并且不可更改,同时JDK9已经不推荐使用CMS,默认使用G1,并且JDK14已被删除了。CMS牺牲了吞吐量来追求回收速度,低延迟,低停顿。可以使用JVM启动参数:-XX:+UseConMarkSweepGC来开启CMS。

  CMS回收过程

  CMS回收过程有几个大阶段:

  1. 初始标记(CMS initial mark)
  2. 并发标记(CMS concurrent mark)
  3. 重新标记(CMS remark)
  4. 并发清理(CMS concurrent sweep)

  

 

  阶段一:初始标记

  此阶段会STW,作用是标记初始化的存活对象,分为两部分:

  • 标记老年代中所有GC Roots对象,包括根对象直接引用的对象,如图对象1;
  • 标记年轻代中存活的对象引用的老年代对象,如图对象2、3;

 

  而外的,在JAVA语言中可当作GC Roots对象包括(两栈两方法):

  1. 虚拟机栈(栈帧中本地变量表)中引用的对象;
  2. 方法区中类静态变量引用的对象;
  3. 方法区中常量引用的对象;
  4. 本地方法栈中JNI引用的对象;

  PS:  为了加快此阶段处理速度,减少停顿时间,可以开启初始化阶段并行化 -XX:+CMSParallelInitialMarkEnabled JDK7之前默认单线程,JDK8之后默认多线程。

  阶段二:并发标记

  此阶段,CMS GC 从“初始标记”阶段标记的对象开始找出所有存活的对象,并发标记阶段因为与用户线程同时运行,不用STW,这样会导致对象的状态发生变法,如:年轻代对象从年轻代晋升到老年代、有些对象直接分配到老年代、老年代喝年轻代对象引用发生变化,这些都需要后续重新标记的时候识别出来,否则会发生浮动垃圾、漏标的情况。为了后续重新标记的效率,本阶段会对上述对象变化情况所在的Card标记为Dirty,后续只需要扫描这些Dirty Card的对象,避免扫描整个老年代;本并发标记阶段只负责将引用发生变化的Card标记为Dirty状态,不负责处理。

  如下图所示,也就是对象1、2、3,最终会找到对象4和5。并发标记的特点是和应用程序线程同时运行。并不是老年代的所有存活对象都会被标记,因为标记的同时应用程序会改变一些对象的引用等。

 

   阶段三:并发预处理

  此阶段不会STW,前一阶段已经说明了,不能标记出老年代所有的存活对象,是应为标记是与用户线程并发执行的,会改变一些对象引用,而,并发预处理这个阶段是用来处理前一个阶段变化的对象,它会扫描标记为Dirty的Card,如下图所示,在并发清理阶段,节点3的引用指向了6;则会把节点3的card标记为Dirty,最后将6标记为存活;可使用 -XX:-CMSPrecleaningEnabled 来关闭,默认开启,其目的是为了让最终/重新标记阶段的STW时间尽可能短。

  

 

   阶段四:可终止的并发预处理

  此阶段不会STW,这个阶段尝试在会STW的重新标记阶段之前尽可能多做一些工作,减少重新标记阶段的压力,此阶段的持续时间依赖很多因素,比如:重复次数(CMSMaxAbortablePrecleanLoops),Eden的使用率(XX:CMSScheduleRemarkEdenPenetration)、持续时间(CMSMaxAbortablePrecleanTime)等等,在满足这些条件之前,会循环做与并发预处理一样的事情。

  ps: 此阶段默认持续时间为5s,之所以需要持续5s,是希望这5秒内,能供发生一次young gc,清理掉年轻代的引用,为下个阶段重新标记,扫描年轻代指向老年代引用的时间减少。也可以设置参数 CMSScavengeBeforeRemark 来使重新标记前强制进行一次Minor GC,清理年轻代引用。

  阶段五:重新标记

  这个阶段会导致第二次STW,该阶段任务是完成标记整个老年代对象。

  这个阶段,重新标记整个堆,包括新生代对象+GC Roots+被标记为“脏”区的对象。因为对于老年代的对象,如果被新生代对象引用,那么就会被认为是存活对象,CMS都会把新生代当作GC Roots,这样如果新生代对象太多,扫描时间太久,会影响回收效率,可以加入参数-XX:+CMSScavengeBeforeRemark,在重新标记之前,先执行一次ygc,回收掉年轻带的对象无用的对象,这样只需要扫描幸存下来的对象,那就已经大大减少了扫描时间。所以之前的并发预处理,就需要尽可能的将年轻代清理干净。

  阶段六:并发清理

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

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

  使用CMS需要注意的几点

  • 吞吐量降低,对处理器资源敏感,执行垃圾回收时会占用一部分线程时程序吞吐量降低。
  • 占用CPU资源,与CPU核数挂钩。
  • 内存碎片问题,由于CMS使用的是标记清除算法,这种会产生内存碎片,导致后续大对象无法分配,就会触发Full GC。可以使用参数-XX:+UseCMSCompactAtFullCollection(默认开启,JDK9废弃),在进行Full GC 之前进行一次内存整理。虽然空间碎片解决了,但是停顿时间也增长了,CMS还提供一个参数-XX:CMSFullGCBeforeCompaction=n(默认为0,表示每次进入Full GC时都进行碎片整理),参数作用是当CMS执行n此不整理内存碎片后,下一次进入Full GC前先进行碎片整理。
  • 无法处理浮动垃圾:在并发回收阶段时,当用户线程并发创建了一个对象年轻代放不下,直接晋升到老年代或者年轻代对象超过存活次数晋升到老年代,由于存在这种现象,因此CMS垃圾回收器就必须预留一部分空间给用户线程,不能等老年代满了才去回收(可通过 -XX:CMSInitiatingOccupancyFraction_=数值_-XX:+UseCMSInitiatingOccupancyOnly来设置)

  PS: 当只设置了-XX:CMSInitiatingOccupancyFraction,那么仅有第一次使用设定值,后续CMS自动调整占用率。此设置会失效,所以需要设置-XX:+UseCMSInitiatingOccupancyOnly来固定占用率多少开启回收

  当设置的-XX:CMSInitiatingOccupancyFraction 过大时,就可能会出现在垃圾回收过程中,无法分配对象的问题,导致 并发失败 (Concurrent Mode Failure),此时会临时启用Serial Old回收器来重新进行老年代回收,这样会导致停顿时间更长。

  总结:

  1. CMS回收器只回收老年代,其是以吞吐量为代价换取回收速度
  2. CMS回收过程分为:初始标记、并发标记、并发预处理阶段、可终止预处理、重新标记以及并发清理阶段。其中初始标记、重新标记需要STW,CMS大部分时间花费在重新标记阶段,可以让虚拟机先进性一次Young GC,减少停顿时间。CMS无法解决“浮动垃圾”问题。
  3. 由于CMS回收线程与用户线程并发,可能回收过程中会出现并发模式失败 Concurrent mode failure,解决方法是让CMS尽早GC,可以通过参数在一定次数的Full GC 之后让CMS对内存做一次整理,减少内存碎片。
posted @ 2022-09-14 11:09  梅晓煜  阅读(1921)  评论(0)    收藏  举报