GC(二)CMS

什么是CMS

  CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,

使用场景

  GC过程短暂停,适合对时延要求较高的服务,用户线程不允许长时间的停顿。

实现机制

  1、不压缩老年代,而是使用空闲列表来管理回收空间。

  2、大部分标记清理工作与应用程序并发执行。

存在问题:

  1.对CPU资源敏感

    其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,随着CPU数量的增加而下降。

  2.无法处理浮动垃圾

     CMS并发清理时,用户程序的运行也会产生新的垃圾(一边打扫房间,一遍丢新的垃圾),但是这部分垃圾产生于标记过程之后,因此只好留在下次GC时清理,这种垃圾被称为浮动垃圾(Floating Garbage)。

  3.Concurrent Mode Failure

    由于CMS并发清理阶段,用户程序还在运行,也需要内存空间,因此CMS收集器不能像其他老年代收集器那样,等到老年代空间快满了再执行垃圾收集,而是要预留一部分内存给用户程序使用。CMS的做法是老年代空间占用率达到某个阈值时触发垃圾收集,有一个参数来控制触发百分比: -XX:CMSInitiatingOccupancyFraction=80 (这里配置的是80%)。

    如果预留的老年代空间不够应用程序的使用,就会出现Concurrent Mode Failure,此时会触发一次FullGC,使用Serial Old收集器重新对老年代进行垃圾回收,会发生stop-the-world,耗时相当感人(实际工作中遇到的大部分FGC估计都是这种情况)。Concurrent Mode Failure一般会伴随ParNew promotion failed,晋升担保失败。所谓晋升担保,就是为了应对新生代GC后存活对象过多,Survivor区无法容纳的情况,需要老年代有足够的空间容纳这些对象,如果老年代没有足够的空间,就会产生担保失败。

    为了避免Concurrent Mode Failure,可以采取的做法是:

      1.调大老年代空间;

      2.调低CMSInitiatingOccupancyFraction的值,但这样会造成更频繁的CMS GC;

      3.代码层面优化,控制对象创建频率。

  4.空间碎片

    这是「标记-清理」算法的通病,空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCom-paction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入FullGC时都进行碎片整理)。

CMS收集器的GC周期

  主要由7个阶段组成,其中有两个阶段会发生stop-the-world,其他阶段都是并发执行的。

Phase 1: Initial Mark(初始化标记)

初始化标记阶段,是CMS GC的第一个阶段,也是标记阶段的开始。主要工作是标记可直达的存活对象

主要标记过程

  • 从GC Roots遍历可直达的老年代对象
  • 遍历被新生代存活对象所引用的老年代对象

程序执行情况

  • 支持单线程或并行标记。
  • 发生stop-the-world,暂停所有应用线程。



 

(Marked obj:老年代绿色圆点表示被初始化标记的对象。)

在java中,可作为GC Roots的对象有:

  1.虚拟机栈(栈帧中的本地变量表)中引用的对象;

  2.方法区中的类静态属性引用的对象;

  3.方法区中常量引用的对象;

  4.本地方法栈中JNI(即一般说的Native方法)中引用的对象

Phase 2: Concurrent Mark(并发标记)

并发标记阶段,是CMS GC的第二个阶段。

在该阶段,GC线程和应用线程将并发执行。也就是说,在第一个阶段(Initial Mark)被暂停的应用线程将恢复运行。

并发标记阶段的主要工作是,通过遍历第一个阶段(Initial Mark)标记出来的存活对象,继续递归遍历老年代,并标记可直接或间接到达的所有老年代存活对象。



 

(Current obj:该对象的引用关系发生变化,对下一个对象的引用被删除)

由于在并发标记阶段,应用线程和GC线程是并发执行的,因此可能产生新的对象或对象关系发生变化,例如:

  • 新生代的对象晋升到老年代;
  • 直接在老年代分配对象;
  • 老年代对象的引用关系发生变更;

对于这些对象,需要重新标记以防止被遗漏。为了提高重新标记的效率,本阶段会把这些发生变化的对象所在的Card标识为Dirty,这样后续就只需要扫描这些Dirty Card的对象,从而避免扫描整个老年代。

Phase 3: Concurrent Preclean(并发预清理)

通过参数CMSPrecleaningEnabled选择关闭该阶段,默认启用。

在并发预清理阶段,将会重新扫描前一个阶段标记的Dirty对象(新生代晋升的对象新分配到老年代的对象以及在并发阶段被修改了的对象),并标记被Dirty对象直接或间接引用的对象,然后清除Card标识。

标记被Dirty对象直接或间接引用的对象:



 

清除Dirty对象的Card标识:



 

Phase 4: Concurrent Abortable Preclean(可中止的并发预清理)

本阶段尽可能承担更多的并发预处理工作,从而减轻在Final Remark阶段的stop-the-world。

该阶段发生的前提是,新生代Eden区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold 默认是2M,如果新生代的对象太少,就没有必要执行该阶段,直接执行重新标记阶段。

在该阶段,主要循环的做两件事:

  • 处理 From 和 To 区的对象,标记可达的老年代对象;
  • 和上一个阶段一样,扫描处理Dirty Card中的对象。

具体执行多久,取决于许多因素,满足其中一个条件将会中止运行:

  • 可以设置最多循环的次数 CMSMaxAbortablePrecleanLoops,默认是0,意思没有循环次数的限制。
  • 如果执行这个逻辑的时间达到了阈值CMSMaxAbortablePrecleanTime,默认是5s,会退出循环。
  • 如果新生代Eden区的内存使用率达到了阈值CMSScheduleRemarkEdenPenetration,默认50%,会退出循环。(这个条件能够成立的前提是,在进行Precleaning时,Eden区的使用率小于十分之一)

如果在循环退出之前,发生了一次YGC,对于后面的Remark阶段来说,大大减轻了扫描年轻代的负担,但是发生YGC并非人为控制,所以只能祈祷这5s内可以来一次YGC。

Phase 5: Final Remark(重新标记)

预清理阶段也是并发执行的,并不一定是所有存活对象都会被标记,因为在并发标记的过程中对象及其引用关系还在不断变化中。

因此,需要有一个stop-the-world的阶段来完成最后的标记工作,这就是重新标记阶段(CMS标记阶段的最后一个阶段)。主要目的是重新扫描之前并发处理阶段的所有残留更新对象。

主要工作:

  • 遍历新生代对象,重新标记;(新生代会被分块,多线程扫描)
  • 根据GC Roots,重新标记;
  • 遍历老年代的Dirty Card,重新标记。这里的Dirty Card,大部分已经在Preclean阶段被处理过了。

CMS算法中提供了一个参数:CMSScavengeBeforeRemark,默认并没有开启,如果开启该参数,在执行该阶段之前,会强制触发一次YGC,可以减少新生代对象的遍历时间,回收的也更彻底一点。不过,这种参数有利有弊,利是降低了Remark阶段的停顿时间,弊的是在新生代对象很少的情况下也多了一次YGC,最可怜的是在AbortablePreclean阶段已经发生了一次YGC,然后在该阶段又傻傻的触发一次。所以利弊需要把握。

Phase 6: Concurrent Sweep(并发清理)

并发清理阶段,主要工作是清理所有未被标记的死亡对象,回收被占用的空间。



 

Phase 7: Concurrent Reset(并发重置)

并发重置阶段,将清理并恢复在CMS GC过程中的各种状态,重新初始化CMS相关数据结构,为下一个垃圾收集周期做好准备。

参考文档:

https://www.cnblogs.com/littleLord/p/5380624.html

https://www.iteye.com/blog/zhanjia-2435266

原文地址:https://www.cnblogs.com/amei0/p/11738010.html