CMS垃圾收集器

@(博客随笔)[jvm, gc]

CMS垃圾收集器

CMS概念

cms全称ConcurrentMarkSweep,是一款使用并发、标记-清除算法的垃圾回收器。 如果老年代使用CMS垃圾回收器,需要添加虚拟机参数-“XX:+UseConcMarkSweepGC”。

触发cms垃圾回收的机制

根据cms的出发机制分为主动性GC被动性GC,被动GC又称为周期性GC。

周期(被动)性Old GC

周期性Old GC,执行的逻辑也叫Background Collect,对老年代进行回收,在GC日志中比较常见,由后台线程ConcurrentMarkSweepThread循环判断(默认2s)是否需要触发。
周期性GC
周期性GC触发条件:

  1. 老年代使用率达到阈值 CMSInitiatingOccupancyFraction,默认92%
  2. 永久代的使用率达到阈值 CMSInitiatingPermOccupancyFraction,默认92%,前提是开启 CMSClassUnloadingEnabled
  3. 新生代的晋升担保失败(老年代是否有足够的空间来容纳全部的新生代对象或历史平均晋升到老年代的对象,如果不够的话,就提早进行一次老年代的回收,防止下次进行YGC的时候发生晋升失败。)

主动性Old GC

这个主动Old GC的过程,触发条件比较苛刻:

  1. YGC过程发生Promotion Failed,进而对老年代进行回收
  2. System.gc(),前提是添加了-XX:+ExplicitGCInvokesConcurrent参数

主动性GC开始时,需要判断本次GC是否要对老年代的空间进行Compact(因为长时间的周期性GC会造成大量的碎片空间),判断逻辑实现如下:

*should_compact =

UseCMSCompactAtFullCollection&&

((_full_gcs_since_conc_gc >=CMSFullGCsBeforeCompaction)||

GCCause::is_user_requested_gc(gch->gc_cause())||

gch->incremental_collection_will_fail(true/* consult_young */));

在三种情况下会进行压缩:

  1. 其中参数 UseCMSCompactAtFullCollection(默认true)和CMSFullGCsBeforeCompaction(默认0),所以默认每次的主动GC都会对老年代的内存空间进行压缩,就是把对象移动到内存的最左边。
  2. 执行了 System.gc(),也会进行压缩。
  3. 新生代的晋升担保失败。

垃圾回收的过程

虽然CMS称为并发垃圾收集器,但是它还是有STW的过程,只不过这个时间可以动态调整,而且相比其他垃圾收集器缩短很多。

初始堆结构(没有分配任何内容)
Alt text
JVM运行一段时间之后
Alt text
当条件满足时,采用“标记-清理”算法对老年代进行回收,过程可以说很简单,标记出存活对象,清理掉垃圾对象,但是为了实现整个过程的低延迟,实际算法远远没这么简单,整个过程分为如下几个部分:
Alt text
CMS GC完成之后
Alt text

0、三色标记法

对象在标记过程中,根据标记情况,分成三类:

  1. 白色对象,表示自身未被标记;
  2. 灰色对象,表示自身被标记,但内部引用未被处理;
  3. 黑色对象,表示自身被标记,内部引用都被处理;

1、Initial Mark(Stop the World)

2018-12-26T19:01:54.023+0800: 5163.251: [GC [1 CMS-initial-mark: 2359441K(4718592K)] 2376231K(6029312K), 0.0858500 secs] [Times: user=0.03 sys=0.02, real=0.08 secs]

该阶段单线程执行,主要分分为两步:

  1. 标记GC Roots可达的老年代对象;
  2. 遍历新生代对象,标记可达的老年代对象;
    Alt text

2、Concurrent Marking

2.1、GC ROOT TRACING
2018-12-26T19:01:54.110+0800: 5163.338: [CMS-concurrent-mark-start]
2018-12-26T19:01:55.115+0800: 5164.343: [CMS-concurrent-mark: 1.004/1.005 secs] [Times: user=1.73 sys=0.12, real=1.01 secs]

该阶段进行GC ROOT TRACING,在第一个阶段被暂停的线程重新开始运行。
由前阶段标记过的对象出发,所有可到达的对象都在本阶段中标记。

The mutators are executing during the concurrent phases 2, 3, and 5 and any objects allocated in the CMS generation during these phases (including promoted objects) are immediately marked as live.
意思是:mutators在并发标记、重标记、重置这三个阶段执行,在这三个阶段期间分配到老年代(包括晋升)的对象都标记为存活的。

2.2、并发预清理

为什么需要这个阶段,存在的价值是什么?
因为CMS GC的终极目标是降低垃圾回收时的暂停时间,所以在该阶段要尽最大的努力去处理那些在并发阶段被应用线程更新的老年代对象,这样在暂停的重新标记阶段就可以少处理一些,暂停时间也会相应的降低。所以该阶段做的工作和重新标记阶段是一样的。

2018-12-26T19:01:55.115+0800: 5164.343: [CMS-concurrent-preclean-start]
2018-12-26T19:01:55.143+0800: 5164.371: [CMS-concurrent-preclean: 0.027/0.028 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]

在并发标记阶段(GC线程和应用线程同时执行,好比你妈在打扫房间,你还在扔纸屑),可能产生新的引用关系如下:

  1. 老年代的新对象被GC Roots引用
  2. 老年代的未标记对象被新生代对象引用
  3. 老年代已标记的对象增加新引用指向老年代其它对象
  4. 新生代对象指向老年代引用被删除
  5. 也许还有其它情况…
    Alt text

首先我们想怎么找出这些对象并将其标记为活着的?

通过参数CMSPrecleaningEnabled选择关闭该阶段,默认启用,主要做两件事情:

  1. 处理新生代已经发现的引用,比如在并发阶段,在Eden区中分配了一个A对象,A对象引用了一个老年代对象B(这个B之前没有被标记),在这个阶段就会标记对象B为活跃对象。
  2. 在并发标记阶段,如果老年代中有对象内部引用发生变化,会把所在的Card标记为Dirty,通过扫描这些Card Table,重新标记那些在并发标记阶段引用被更新的对象。

card table
CMS将老年代的空间分成大小为512bytes的块,card table中的每个元素对应着一个块。并发标记时,如果某个对象的引用发生了变化,就标记该对象所在的块为 dirty card。
举个例子:
并发标记时对象的状态:
Alt text
但随后current obj的引用发生了变化:
Alt text
current obj所在的块被标记为了dirty card。
随后到了pre-cleaning阶段,还记得该阶段的任务之一就是标记这些在并发标记阶段被修改了的对象么?之后那些通过current obj变得可达的对象也被标记了,变成下面这样:
Alt text
同时dirty card标志也被清除。

在hotspot中,card table是一个byte数组,
hotspot 虚拟机使用字节码解释器、JIT编译器、 write barrier维护 card table。
https://blogs.msdn.microsoft.com/abhinaba/2009/03/02/back-to-basics-generational-garbage-collection/

2.3、可中断的并发预清理
2018-12-26T19:01:55.143+0800: 5164.371: [CMS-concurrent-abortable-preclean-start]
2018-12-26T19:01:58.757+0800: 5167.984: [GC2018-12-26T19:01:58.757+0800: 5167.985: [ParNew: 1065159K->27935K(1310720K), 0.0551130 secs] 3424601K->2387768K(6029312K), 0.0555470 secs] [Times: user=0.19 sys=0.01, real=0.05 secs]
 CMS: abort preclean due to time 2018-12-26T19:02:00.335+0800: 5169.563: [CMS-concurrent-abortable-preclean: 5.086/5.192 secs] [Times: user=10.00 sys=0.58, real=5.20 secs]
2018-12-26T19:02:00.336+0800: 5169.564: [GC[YG occupancy: 493219 K (1310720 K)]2018-12-26T19:02:00.337+0800: 5169.564: [Rescan (parallel) , 0.1170360 secs]2018-12-26T19:02:00.454+0800: 5169.681: [weak refs processing, 0.0138130 secs]

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

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

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

当然了,这个逻辑不会一直循环下去,打断这个循环的条件有三个:

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

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

3、重新标记

2018-12-26T19:02:00.468+0800: 5169.695: [scrub string table, 0.0179210 secs] [1 CMS-remark: 2359832K(4718592K)] 2853052K(6029312K), 0.1500140 secs] [Times: user=0.50 sys=0.00, real=0.15 secs]

上述对象中可能有一些已经在Precleaning阶段和AbortablePreclean阶段被处理过,但总存在没来得及处理的,所以还有进行如下的处理:

  1. 遍历新生代对象,重新标记
  2. 遍历老年代的Dirty Card,重新标记,这里的Dirty Card大部分已经在clean阶段处理过
  3. 遍历GC ROOTS,重新标记

在第一步骤中,需要遍历新生代的全部对象,如果新生代的使用率很高,需要遍历处理的对象也很多,这对于这个阶段的总耗时来说,是个灾难(因为可能大量的对象是暂时存活的,而且这些对象也可能引用大量的老年代对象,造成很多应该回收的老年代对象而没有被回收,遍历递归的次数也增加不少),如果在AbortablePreclean阶段中能够恰好的发生一次YGC,这样就可以避免扫描无效的对象。

如果在AbortablePreclean阶段没来得及执行一次YGC,怎么办?

CMS算法中提供了一个参数: CMSScavengeBeforeRemark,默认并没有开启,如果开启该参数,在执行该阶段之前,会强制触发一次YGC,可以减少新生代对象的遍历时间,回收的也更彻底一点。

不过,这种参数有利有弊,利是降低了Remark阶段的停顿时间,弊的是在新生代对象很少的情况下也多了一次YGC,最可怜的是在AbortablePreclean阶段已经发生了一次YGC,然后在该阶段又傻傻的触发一次。

4、并发清理

用户线程被重新激活,同时清理那些无效的对象。

2018-12-26T19:02:00.487+0800: 5169.715: [CMS-concurrent-sweep-start]
2018-12-26T19:02:02.637+0800: 5171.865: [GC2018-12-26T19:02:02.638+0800: 5171.865: [ParNew: 1076511K->14949K(1310720K), 0.0431730 secs] 1812643K->751379K(6029312K), 0.0436710 secs] [Times: user=0.16 sys=0.00, real=0.04 secs]
2018-12-26T19:02:02.910+0800: 5172.138: [CMS-concurrent-sweep: 2.370/2.423 secs] [Times: user=5.05 sys=0.24, real=2.42 secs]

5、重置

CMS清除内部状态,为下次回收做准备。 如card table等

2018-12-26T19:02:02.910+0800: 5172.138: [CMS-concurrent-reset-start]
2018-12-26T19:02:03.087+0800: 5172.314: [CMS-concurrent-reset: 0.176/0.176 secs] [Times: user=0.38 sys=0.18, real=0.18 secs]

cms的优缺点

1、用户线程执行效率
2、浮动垃圾
3、空间碎片

JVM参数

-XX:+UseConcMarkSweepGC
该标志首先是激活CMS收集器。默认HotSpot JVM使用的是并行收集器。
-XX:CMSInitiatingOccupancyFraction
当老年代使用率达到此参数定义的值时,会触发CMS GC
-XX:+UseCMSInitiatingOccupancyOnly
我们用-XX+UseCMSInitiatingOccupancyOnly标志来命令JVM不基于运行时收集的数据来启动CMS垃圾收集周期。而是,当该标志被开启时,JVM通过CMSInitiatingOccupancyFraction的值进行每一次CMS收集,而不仅仅是第一次。
-XX:+CMSIncrementalMode
该标志将开启CMS收集器的增量模式。增量模式经常暂停CMS过程,以便对应用程序线程作出完全的让步。

参考

1.Getting Started with the G1 Garbage Collector
2.Back To Basics: Generational Garbage Collection
3.concurrent-mark-and-sweep
4.图解 CMS 垃圾回收机制原理,-阿里面试题
5.详解CMS垃圾回收机制

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值