CMS垃圾收集器运行原理

目录

概述

执行过程

初始标记initial mark

并发标记 concurrent mark

三色表示

遍历算法

标记过程举例

并发过程中变化的维护

并发预清理concurrent preclean

可中断预清理concurrent abortable preclean

重新标记final remark

并发清除concurrent sweep

并发重置concurrent reset

缺点

cpu资源敏感,降低吞吐量

浮动垃圾

内存碎片

运行过程常见问题

concurrent mode failure

promotion failed

OutOfMemoryError 


 

 

概述

CMS垃圾收集器是一款优秀的老年代并发垃圾收集器,通过与用户线程并发执行的方式减少GC停顿的时间。本文主要聊一下CMS设计到的相关的数据结构、具体的执行过程、运行中会出现的异常情况。

在CMS之前并行垃圾收集器通过下图方式进行,虽然GC阶段多线程并行执行单此时用户线程是完全暂停的。如果GC时间过长,将引发服务响应超时、调用接口超时等各类异常。

而CMS垃圾收集器大部分时间GC线程与用户线程并发执行,只有在初始标记和重新标记阶段才暂停用户线程

总体思路:当达到GC条件时,开始并发标记存活的对象,并发的过程中记录对象引用关系的变化。并发标记结束后,暂停用户线程,处理引用关系变化,得到所有存活的对象和可以清理的对象。最终并发清理掉不被使用的对象(存在已经不被引用但本次清理不掉的对象)。

执行过程

初始标记initial mark

暂停用户线程,标记GC root和新生代直接关联的对象。

这里的GC root关联的对象包含虚拟机栈中引用的对象、类静态属性引用的对象、本地方法栈中JNI引用的对象。另外,CMS是老年代的垃圾收集器,被新生代引用的对象应该被标记为存活,所以这里还包含新生代对象。

并发标记 concurrent mark

启动用户线程,三色遍历法遍历标记老年代的所有对象。

三色表示

  • 黑色对象:自己被标记,且引用对象已经处理完成。
  • 灰色对象:自己被标记,但引用对象未处理。
  • 白色对象:没有对它做标记。

     标记过的意思就是认为这个对象是存活的,本次GC不回收这个对象。

遍历算法

说到对象的遍历方式,自然而然会想到借助栈或队列进行图遍历。对象之间的关系正好构建成一个图,对象就是节点,引用关系就是边。但是图遍历有这样一个问题: GC root和新生代的对象都要装进栈或队列当中,可能会导致占用特别大的额外空间。

为了避免上的问题,CMS采用了线性遍历的方式。

首先,引入一个bit数组,用它表示内存中每个位置的状态,每个bit位代表4字节的内存空间,所以它的大小是固定的。

遍历的策略也直接引用论文中的图片,感兴趣的同学可以细品~

遍历bitmap,找到被标记为存活的对象cur,将它压入栈中,然后开始遍历这个对象出发的所有对象。遍历栈的过程中,如果遇到地址比cur低的对象则标记并压如栈中,遇到地址比cur高则只标记不入栈。所有GC root引用的对象已经在初始标记阶段标记成了存活对象,遍历过程遇到其中一个就开始利用栈遍历它及它之前的所有对象,这就保证了栈中最多有1个GC root直接引用的对象,有效控制了栈空间的大小。

标记过程举例

标记的过程伴随着对象引用的修改,下图举例说明了并发标记的过程

  • 初始标记(图中未体现,参考步骤a):初始标记时对a进行了标记。
  • 步骤a:对a引用的对象bc,及b引用的对象e进行标记,根据当时的对象引用关系,abcegd是存活的。
  • 步骤b:对象引用关系发生了改变,b不再引用c新增引用d,g不再引用d
  • 步骤c:完成了并发标记的过程,abceg被标记。第二个和第四个区域内的对象引用关系发生了改变,被记录了下来。这里面运用到了card table、mod union table数据结构和write barrier技术,后面进行说明。
  • 步骤d:实际上是重新标记后的结果,可以看到对象d在并发标记结束时未进行标记,但是它还在被对象b引用,不应该回收。这就依赖重新标记阶段对dirty card(对象引用关系发生变化的区域)的处理。

从上面的例子中可以看到CMS的一些特性:

  • 初始标记时,不可达的对象,在本次GC中一定会被回收,如对象f
  • 已经标记过的对象,即使最终已经不被引用本次GC也不会回收,如对象c

并发过程中变化的维护

card table与mod union table

card table是一个数组,数组中每个位置存的是一个byte,每个比特位有不同的作用(具体的作用可能得研究下源码,没有找到相关的资料)。CMS将老年代的空间分成大小为512bytes的块,card table中的每个元素对应着一个块。

对于新生代:它记录老年代到新生代的引用,younggc时不用遍历整个老年代

对于老年代:它记录并发标记开始引用发生变化的card,并发标记结束后需要处理这些card

由于新生代GC与老年代GC同时使用card table,所以会出现冲突的情况。新生代GC时,发现老年代的dirty card(card的一种状态)没有指向新生代的引用,会将这个card设置为clean(改变了老年代对象引用发生设置的状态),但这个card必须在remark阶段进行重新标记。所以增加了另一个数据结构mod union table解决此问题。

mod union table是一个bit位向量,一个bit表示一个card的状态。

它由新生代垃圾收集器维护,新生代GC将card设置为clean之前,把mod union table设置为dirty。card table状态为dirty、或者mod union table标记为dirty、或者同时两种数据结构都标记为dirty的card表示并发标记阶段引用发生了变化,需要在后面的阶段进行处理。

write barrie 

write barrie写屏障类似于一个切面,用户线程写对象引用的时候就触发write barrier的逻辑,将对象所处的card设置为dirty。

并发预清理concurrent preclean

处理dirty card,降低remark阶段暂停时间。

重新标记的过程是STW的,所以为了缩短停顿时间,在并发标记之前应该尽可能多的完成重新标记阶段的工作。并发预清理就是对dirty card进行遍历处理,降低重新标记需要处理的dirty card的数量。

可中断预清理concurrent abortable preclean

继续处理dirty card,满足条件时停止当前阶段,进入下一阶段。

可中断预清理也会处理dirty card,替remark阶段分担一部分工作。这个阶段更主要的目的是控制remark和新生代GC分开执行,避免连续两次暂停导致总的暂停时间过长,其中运用了一些策略。

预清理阶段结束之后,如果Eden空间大于CMSScheduleRemarkEdenSizeThreshold(默认2M),则进入可中断预清理阶段。

当Eden空间达到CMSScheduleRemarkEdenPenetration(默认50%)时进入remark阶段。

如果等待超过了CMSMaxAbortablePrecleanTime(默认5s)同样进入remark阶段。

另外,还有个CMSMaxAbortablePrecleanLoops参数可以控制可中断预清理循环的次数,到达次数则退出预清理阶段进入remark,默认是0不限制次数。

重新标记final remark

暂停用户线程,从GC root(包含新生代对象)出发重新标记,并处理完所有dirty card。

并发阶段,老年代可能面对如下变化,重新标记后全部确定哪些是存活的或者不存活的

  • 晋升到老年代的对象
  • 直接分配到老年代的对象
  • 老年代中引用关系改变的对象
  • 新生代到老年代引用关系的改变
  • GC root到老年代引用关系的改变

新生代大小的影响和控制

重新标记阶段需要遍历新生代对象,但新生代里大多都是垃圾,如果remark之前发生一次新生代GC,则会大大减小remark阶段需要遍历的对象数量。可以设置CMSScavengeBeforeRemark参数强制在remark之前执行一次新生代GC。但是正如可中断预清理阶段的分析,新生代GC也是有停顿的,这样两次停顿连在一起也可能会很长,需要进行权衡。

并发清除concurrent sweep

并发清除标记为不可达的对象,回收并合并空闲内存。

并发重置concurrent reset

重新设置CMS相关的各种状态及数据结构,为下一个垃圾收集周期做好准备。

缺点

cpu资源敏感,降低吞吐量

CMS没有运行的时候所有全部cpu资源都供用户线程使用,CMS开始并发运行后就要跟用户线程竞争cpu资源,导致应用线程运行变慢。对于cpu资源非常紧缺的系统,假设只有2核,CMS运行起来后将占用一半的cpu资源,用户线程将感知到运行速度减半。

并发带来的好处是可以降低用户线程的停顿时间,对于在线服务类应用非常有益,因为长时间的停顿可能导致响应超时等问题。但相对于非并发垃圾收集器,CMS整个周期内很多工作是重复的(比如重新标记阶段对dirty card中的对象重新标记,而在并发标记阶段可能已经标记过了),导致整体的吞吐量是降低的。

浮动垃圾

因为CMS垃圾收集器的特性,被标记过的对象,即使最终变成垃圾本次GC也不会回收它,这些垃圾就是浮动垃圾。浮动垃圾的产生意味着内存里不光装着存活对象,还要装着这些浮动垃圾,所以容纳同样多的存活对象CMS需要占用更大的内存空间。

内存碎片

CMS使用标记清除算法,收集结束之后会产生大量内存碎片。当有大对象需要分配空间时,可能总的空间大小是足够的,但是没有连续的空间装下此对象。

CMS默认开启UseCMSCompactAtFullCollection 参数,在FullGC时进行内存碎片的合并整理。内存碎片虽然解决了,但负面影响就是停顿时间变长了。还有另外一个CMSFullGCsBeforeCompaction参数可以控制多少次FullGC才会进行整理,默认是0代表每次FullGC都会进行碎片整理。

运行过程常见问题

concurrent mode failure

并发虽好,但会引入一些问题。对于非并发的垃圾收集器,可以等到老年代无法分配对象时再执行GC。但对于cms则需要预留出空间提前开始GC,预留的空间供并发期间新对象的分配及新生代对象的晋升使用。如果在老年代分配对象发现老年代装不下,则会触发concurrent mode failure,此时将会暂停用户线程执行FullGC或者串行模式的CMS。

promotion failed

这个错误涉及到CMS担保机制,新生代GC之前会根据历史晋升到老年代对象的大小,预估本次老年代是否足够容纳新生代晋升的对象。如果预估时空间足够,但新生代GC实际执行时发现容纳不了,则会引起promotion failed错误。

OutOfMemoryError 

CMS垃圾收集器发现大部分时间都浪费在GC上就会抛出OutOfMemoryError异常,具体为98%的时间在GC但回收不到2%的空间。这样做实际上是为了防止程序进入一种虽然在运行实际上一直在GC假死状态,也可以通过设置-XX:-UseGCOverheadLimit禁用该机制。

 

阅读建议:不管是大V或是小号都会有自己的关注点、忽略的点,都有自己理解到位以及理解有偏差的地方。如果有缘读到本文,建议参考下官方文档、论文。至于源码,阅读成本比较高,感觉看一下自己感兴趣的地方就可以了,网上有一些源码理解的分享。

官网:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html

论文:https://www.cs.purdue.edu/homes/hosking/ismm2000/papers/printezis.pdf

  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值