JVM垃圾收集器详解之CMS

CMS(Concurrent Mark Sweep)是HotSpot虚拟机中第一款实现并发收集的垃圾回收器,是为那些希望使用较短的垃圾收集暂停时间并且可以在应用程序运行时与垃圾收集器共享处理器资源的应用程序而设计的,简单来说,CMS就是追求最短停顿时间的垃圾收集器。

CMS主要针对老年代进行垃圾回收,可以配合Serial或者ParNew新生代垃圾收集器进行回收,并且从名字上包含“Mark Sweep”就可以看出CMS收集器是基于标记-清除算法实现的,相对之前的垃圾收集器CMS整个回收过程要稍微复杂一些,大致分为4步:

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

1、初始标记(CMS initial mark)

首先初始标记,需要暂停用户线程,不过这一步仅仅标记GCRoots能直接关联到的对象,因此暂停时间很短。

只标记GCRoots直接可达对象
在这里插入图片描述

2、并发标记

并发标记就是接着初始标记的根对象继续往下标记,这个阶段是最耗时的,但是好在是与用户线程并发执行的。

考虑一种情况,老年代对象被新生代对象引用,如果此时只扫描老年代的GCRoots对象,A对象就会被遗漏,所以并发标记时实际上也会扫描新生代对象。

在这里插入图片描述

3、重新标记

重新标记阶段是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。

4、并发清除

清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

并发预清理阶段

实际上除了上述的主要流程之外,CMS还有一步并发预清理阶段,这个阶段主要是发生在重新标记之前,此阶段工作与重新标记类似,目的主要是为了希望能够在重新标记前触发一次新生代的GC,这样就可以减少重新标记的停顿时间,此阶段主要标记新生代晋升到老年代的对象,直接分配到老年代的对象,并发过程中引用发生修改的对象,默认情况下当eden区达到了2M,则会开启并发预清理阶段,当eden区使用达到50%时停止预清理,或者预清理阶段超过默认时间5秒时也会停止预清理,配置CMSScavengeBeforeRemark参数,也可强制使每次重新标记前都触发一次YGC,但是这样的做法,虽然减少了重新标记的任务,但如果刚好已经执行过一次YGC,重新标记又执行一次,也会造成STW时间变长。

如何解决并发标记时引用关系改变问题?

由于第二阶段垃圾标记是与用户线程并发执行的,那就有可能产生错误标记的问题,比如一个对象我们刚刚标记完,结果用户线程又把其他对象引用到这个刚刚标记完的对象上。

如下图,当垃圾线程标记时,A的这条引用链走到B就已经走完了,但是如果之后用户线程让B对象又引用了C对象,那么C对象就会被漏标,最终会被当做垃圾对象被清理掉,显然C对象是不能被回收的。
在这里插入图片描述
为了解决这样的问题,CMS首先将老年代等份划分成了好多小块,这些小块的集合可以叫做card table(byte数组,数组中每一元素对应一个块),当某一个对象的引用发生变化时(只记录黑色对象引用发生变化),就改变这个对象所在的块的标识,比如标记为:脏card,这样我们在最终标记时只要在遍历一次所有的脏card即可。

这里涉及到三色标记算法,不理解建议参考JVM垃圾回收算法—三色标记法分析这篇文章。

如果确定新生代对象是否存活?

1、GC可达性分析
2、老年代引用新生代对象

GC可达性分析不用多说,主要分析一下老年代引用新生代对象的问题,刚才分析初始标记时就已经了解到,在分代收集中只是扫描GCRoots肯定是不够的,要确认老年代对象是否存活就必须扫描所有新生代对象,所以刚才介绍了CMS并发预清理阶段就是为了来一次新生代的垃圾回收,这样新生代中大多数对象就被回收了。

现在问题是新生代要判断哪些对象被老年代引用了,老年代的对象的都是长期存活的,一次垃圾回收可没用,那就只能全量扫描老年代了?显示CMS不会这样做,这时候card table又派上用场了,当有新生代引用老年代对象时,只需要把老年代所在的card标记新增一个标识即可,就像上面标记为“脏”一样,这样新生代只需要扫描所有有相关标识的card即可。

cardtable是一个byte数组,一个byte有8个位,只要约定好每一位的含义就可以区分标识是对象在并发期间修改了,还是老年代引用新生代对象!

CMS缺点

  1. 因为是并发执行,所以会占用用户线程,CPU核心数小于4的服务器不推荐使用。
  2. 浮动垃圾问题,因为CMS是与用户线程并发执行的,所以并不能等待内存占用达到100%了再回收,jdk6以后默认是92%,就会开启CMS垃圾回收,如果过程中产生Concurrent Mode Failure,则会切换成serial old进行回收。
  3. 垃圾碎片:CMS采用标记-清除算法,因此会存在碎片问题,CMS默认情况下每一次FullGC都会进行一次压缩整理,通过参数可以配置UseCMSCompactAtFullCollection 默认为true, CMSFullGCsBeforeCompaction就是表示配置每多少次CMS的FullGC执行一次压缩,但是如果用户调用system.gc或者担保失败,那也会触发压缩的FullGC。

CMS常见问题解决思路

并发模式失败和晋升失败都会导致长时间的停顿,常见解决思路如下:

1、降低触发 CMS GC 的阈值。即参数 -XX:CMSInitiatingOccupancyFraction 的值,让 CMS GC 尽早执行,以保证有足够的空间。
2、增加 CMS 线程数,即参数 -XX:ConcGCThreads。
3、增大老年代空间。
4、让对象尽量在新生代回收,避免进入老年代。

通常 CMS 的 GC 过程基于标记清除算法,不带压缩动作,导致越来越多的内存碎片需要压缩。
常见以下场景会触发内存碎片压缩:

1、新生代 Young GC 出现新生代晋升担保失败(promotion failed))
2、程序主动执行System.gc()

可通过参数 CMSFullGCsBeforeCompaction 的值,设置多少次 Full GC 触发一次压缩。

默认值为 0,代表每次进入 Full GC 都会触发压缩,带压缩动作的算法为单线程 Serial Old 算法,暂停时间(STW)时间非常长,需要尽可能减少压缩时间。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码拉松

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值