浅谈JVM的垃圾收集(二)——CMS垃圾收集器

背景

作为浅谈JVM的垃圾收集(一)的后续文章,建议先看前文再来读这篇文章。

前言

上一篇文章介绍了三大垃圾收集算法,而垃圾收集器就是垃圾收集算法的具体实现。本文主要介绍垃圾收集器,重点介绍CMS、G1、ZGC和Shenandoah收集器实现的细节。

年轻代收集器
Serial、ParNew、Parallel Scavenge
老年代收集器
Serial Old、Parallel Old、CMS收集器
特殊收集器
G1收集器,跨代收集

浅谈JVM的垃圾收集(二)——CMS垃圾收集器_第1张图片
收集器,连线代表可结合使用

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是基于标记-清除算法实现的,所以会有内存碎片的问题,可机使用 参数-XX:+UseConcMarkSweepGC来开启CMS收集器。它的运作过程分为四个步骤:

1)初始标记(CMS initial mark),即标记根节点,速度很快,要STW(stop the word)
2)并发标记(CMS concurrent mark)即可达性分析的引用链分析,遍历对象图。和用户线程并发执行。
3)重新标记(CMS remark),即增量更新中,以黑色对象为根遍历新插入的白色对象。通俗的说就是,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。要STW,停顿比初始标记稍长。
4)并发清除(CMS concurrent sweep),即清除标记为死亡的对象,和用户线程并发执行

耗时时长:初始标记(STP)<重新标记(STP)<并发标记≈并发清除

CMS收集器具体工作如下:

浅谈JVM的垃圾收集(二)——CMS垃圾收集器_第2张图片

可见GC的所有步骤都必须在safepoint中进行

由于用时最长的并发标记与并发清理都能与用户线程一起并发执行,可见CMS的特点:并发收集、低停顿。但是CMS也有一下三个缺点:

  • CMS对CPU资源很敏感。cms默认配置启动的时候垃圾线程数(cpu核心数量+3)/4多核CPU能充分发挥它的优势,但是如果CPU核数不足4核时,那会导致CPU负载很高,因为还要将一半CPU运算能力给到垃圾回收线程,可能会导致用户线程执行速度降低。
  • CMS收集器无法处理“浮动垃圾”(Floating Garbage),可能导致“Con-current Mode Failure”失败,进而导致Full GC
  • 由于CMS是基于标记清除算法的,所以会产生内存碎片。内存碎片过多会导致大对象无法分配从而触发Full GC。

名词解释:

  • 浮动垃圾:由于cms收集支持用户线程并发运行,程序运行的时候会产生新的垃圾,这里产生的垃圾就是浮动垃圾,cms无法当次收集中处理,得等下次才可以。
  • current Mode Failure:并发失败,CMS GC期间预留的内存无法满足程序分配新对象的需要,触发Full GC。即full gc 的时候 cms gc 还在进行中导致抛这个错。

为了解决内存碎片问题,CMS提供了以下参数(都从JDK9开始废弃):

  • -XX:+UseCMSCompactAtFullCollection:默认开启,用于CMS进行Full GC前是否进行内存碎片的整理合并。由于内存碎片整理要移动对象,且无法并发,开启此参数后可能会导致停顿时间变长,所以有了下面的参数配合使用。
  • -XX:CMSFullGCsBeforeCompaction:要求CMS收集器在执行过若干次(由参数决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理。默认为0。

由于在垃圾收集阶段用户线程还需要持续运行,所以需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。可以通过参数-XX:CMSInitiatingOccupancyFraction指定内存达到老年代空间的百分比时,触发CMS GC。

例如-XX:CMSInitiatingOccupancyFraction=70 指定CMS在对老年代内存占用率达到70%的时候开始GC(因为CMS会有浮动垃圾,所以一般都较早启动GC)。

-CMS 默认采用 JVM 运行时的统计数据判断是否需要触发 CMS GC,如果需要根据 -XX:CMSInitiatingOccupancyFraction 的值进行判断,需要设置参数 -XX:+UseCMSInitiatingOccupancyOnly。一般建议开启这个参数,也方便判断GC的情况。

疑问

如果参数设置得太高导致CMS运行期间预留的内存无法满足程序分配新对象的需要,就会触发Full GC,从而就会出现Concurrent Mode Failure????然后导致冻结用户线程的执行,并临时启用Serial Old收集器来重新进行老年代的垃圾收集, 但这样停顿时间就很长了???

  • 疑问1,到底是并发失败导致full gc还是full gc导致并发失败?它们什么关系?
  • 疑问2,到底预留内存无法满足对象分配时,会不会触发Full GC还是触发使用serial old?还是触发Full GC后再使用serial old?

解答

网上各有各的说法,所以解决疑问的最好的方式就是看HotSpot中关于CMS收集器的源码。

可自己下载源码查看或在gitlab上查看源码:GitHub源码地址
也可见大佬的源码解析文章Hotspot 垃圾回收之CMSCollector(一) 源码解析

CMS有两个GC, 分为 foreground gc(前台GC) 和 background gc(后台GC),由CMS通过参数防止它们并行执行。CMS有个本地线程每隔2s判断是否需要执行老年代的后台GC。

并发失败与Full GC

导致并发失败的真正原因?这个错误出现在这里
浅谈JVM的垃圾收集(二)——CMS垃圾收集器_第3张图片
它被CMSCollector::acquire_control_and_collect调用,而acquire_control_and_collect就是CMS用于执行foreground gc。
浅谈JVM的垃圾收集(二)——CMS垃圾收集器_第4张图片
而first_state也是在acquire_control_and_collect方法,发生在上面代码之前赋值
浅谈JVM的垃圾收集(二)——CMS垃圾收集器_第5张图片
而Idling,看枚举可知background GC 并未结束
浅谈JVM的垃圾收集(二)——CMS垃圾收集器_第6张图片

后台GC在进行时,会检测前台GC是否需要运行,便会让出执行权给前台GC执行,然后前台GC执行时查看GC状态不为空闲(即后台GC已执行完部分步骤),前台GC打印出Concurrent Mode Failure(并发失败),然后前台GC 执行剩下的步骤。

是否进行内存碎片整理

前台GC里包含判断是否进行压缩整理的逻辑。should_compact压缩标志,判断是否进行压缩整理。
浅谈JVM的垃圾收集(二)——CMS垃圾收集器_第7张图片
判断是否进行压缩整理的函数


void CMSCollector::decide_foreground_collection_type(
  bool clear_all_soft_refs, bool* should_compact,
  bool* should_start_over) {
  GenCollectedHeap* gch = GenCollectedHeap::heap();
  assert(gch->collector_policy()->is_two_generation_policy(),
         "You may want to check the correctness of the following");
 
  if (gch->incremental_collection_will_fail(false /* don't consult_young */)) {
    //如果增量收集会失败
    assert(!_cmsGen->incremental_collection_failed(),
           "Should have been noticed, reacted to and cleared");
    _cmsGen->set_incremental_collection_failed();
  }
  //UseCMSCompactAtFullCollection表示在Full GC时是否执行压缩,默认为true
  //CMSFullGCsBeforeCompaction表示一个阈值,Full GC的次数超过该值才会执行压缩
  *should_compact =
    UseCMSCompactAtFullCollection &&
    ((_full_gcs_since_conc_gc >= CMSFullGCsBeforeCompaction) ||
     GCCause::is_user_requested_gc(gch->gc_cause()) || //用户通过System.gc方法请求GC
     gch->incremental_collection_will_fail(true /* consult_young */)); //增量收集失败
  *should_start_over = false;
  //如果should_compact为false且clear_all_soft_refs为true
  if (clear_all_soft_refs && !*should_compact) {
    //当clear_all_soft_refs为true时是否需要压缩,默认为true
    if (CMSCompactWhenClearAllSoftRefs) {
      *should_compact = true;
    } else {
      //如果当前GC已经过FinalMarking环节了,在该环节才处理所有的Refenrence,则需要重新开始一轮GC,
      //重新查找待处理的Refenrence
      if (_collectorState > FinalMarking) {
        //将GC的状态设置为重置
        _collectorState = Resetting; // skip to reset to start new cycle
        //执行重置
        reset(false /* == !asynch */);
        *should_start_over = true;
      } 
    }
  }

主要是这里

*should_compact =
    UseCMSCompactAtFullCollection &&
    ((_full_gcs_since_conc_gc >= CMSFullGCsBeforeCompaction) ||
     GCCause::is_user_requested_gc(gch->gc_cause()) || //用户通过System.gc方法请求GC
     gch->incremental_collection_will_fail(true /* consult_young */)); //增量收集失败

进行压缩整理的条件包括

  • UseCMSCompactAtFullCollection :full gc时开启内存碎片的压缩整理,默认开启。必要条件。
  • CMSFullGCsBefore- Compaction:达到此参数值就进行内存碎片的整理,默认为0
  • 用户调用System.gc方法请求GC
  • 增量收集失败(个人理解是担保失败的一种情况)

具体进行内存碎片整理的full gc的代码,在这个full gc中CMS对老年代的回收降级为使用Serial-Old垃圾回收器进行回收

if (should_compact) {
    //清空discovered References链表中的References实例,Mark-Sweep-Compact代码假定他们的referent都不是NULL且
    //References实例都是存活的
    ref_processor()->clean_up_discovered_references();
 
    if (first_state > Idling) {
      //保存当前堆内存和元空间的使用情况
      save_heap_summary();
    }
    //执行压缩并标记清理,底层核心实现是GenMarkSweep
    do_compaction_work(clear_all_soft_refs);
 
    DefNewGeneration* young_gen = _young_gen->as_DefNewGeneration();
    //获取eden区的最大容量
    size_t max_eden_size = young_gen->max_capacity() -
                           young_gen->to()->capacity() -
                           young_gen->from()->capacity();
    GenCollectedHeap* gch = GenCollectedHeap::heap();
    GCCause::Cause gc_cause = gch->gc_cause();
    size_policy()->check_gc_overhead_limit(_young_gen->used(),
                                           young_gen->eden()->used(),
                                           _cmsGen->max_capacity(),
                                           max_eden_size,
                                           full,
                                           gc_cause,
                                           gch->collector_policy());
  } 

不需要压缩整理执行的标记清理收集

else {
    //执行标记清理
    do_mark_sweep_work(clear_all_soft_refs, first_state,
      should_start_over);
  }
并发失败导致的Full GC

并发失败导致的Full GC包括需要压缩整理的Full GC和foreground collector。压缩整理的Full GC使用MSC(Mark-Sweep-Compact)算法对老年代收集,会有很长的停顿时间。foreground收集使用标记-清理算法,也要STW,但是会省略许多步骤。

个人理解

因为网上说的挺多不同的说法,例如有的说foreground gc包括使用压缩整理的Full GC和使用标记整理的Full GC,都是用Serial Old对老年代收集。有的说只有压缩整理的Full GC才使用Serial Old。下面是个人理解。

压缩整理的Full GC使用的是Serial Old对老年代进行收集,而不用压缩整理的foreground GC也是Full GC,但是使用标记清理算法。

何时触发Full GC
  • 永久代空间(或Java8的元空间)耗尽,直接触发FULL GC
  • 在要进行 young gc 的时候,根据之前统计数据发现年轻代平均晋升大小比现在老年代剩余空间要大,那就会触发 full gc。(其实也是担保失败)
  • 老年代空间不足,大对象直接在老年代申请分配,如果此时老年代空间不足则会触发 full gc。
  • 担保失败即 promotion failure,新生代的 to 区放不下从 eden 和 from 拷贝过来对象,或者新生代对象 gc 年龄到达阈值需要晋升这两种情况,老年代如果放不下的话都会触发 full gc。
  • 执行 System.gc()、jmap -dump 等命令会触发 full gc。

疑问总结

CMS包括foreground GC(前台回收) 和background GC(后台回收)。CMS有个后台线程每隔2s判断是否达到条件需要后台回收。

当触发并发失败时,就是进入了前台回收,即要进行Full GC了,但是否进行压缩整理需要根据条件判断。

并发失败的导致:当后台回收正在执行时,触发了Full GC就会报并发失败。

大佬的肩膀

JVM 源码解读之 CMS 何时会进行 Full GC
CMS几种GC模式解读
Hotspot 垃圾回收之CMSCollector(一) 源码解析
炸了!一口气问了我18个JVM问题!
关于CMS垃圾回收失败是不是进行FULL GC问题

你可能感兴趣的:(JVM垃圾收集,jvm,java)