概述

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

初始标记
暂停应用程序线程,遍历GC ROOTS直接可达的对象并将其压入标记栈(mark-stack)。标记完之后恢复应用程序线程。
并发标记
这个阶段虚拟机会分出若干线程(GC 线程)去进行并发标记。标记哪些对象呢,标记那些GC ROOTS最终可达的对象。具体做法是推出标记栈里面的对象,然后递归标记其直接引用的子对象(如果遇到地址比当前对象低的对象则标记并压如栈中,遇到地址比当前对象高则只标记不入栈),同样的把子对象压到标记栈中,重复推出,压入。。。直至清空标记栈。这个阶段GC线程和应用程序线程同时运行。

三色标记详解:三色标记法与读写屏障 – 简书
这种条件下可能会出现活动对象的漏标的情况,比如下面场景:

活动对象被遗漏标记
A是活动对象,A->B,标记B可达,将其压入标记栈,此时A所有直接子对象遍历完,A出栈,标记线程将不会再访问A。
同时应用程序移掉了B对C的引用,让A重新引用C。
B出栈时无法标记C可达,A虽然引用C但标记线程不会再访问A,此时C会被当成不可达对象。
并发过程中变化的维护card table与mod union table
card table
CMS中一个与YGC相关并十分重要的数据结构是:卡表(card table)。之所以出现卡表这样的一个数据结构是因为:YGC时为了标记活动标记对象除了tracing GC ROOTS之外, 别忘了老年代里也可能会引用新生代对象。所以正常来说还要扫描一次老年代,如果是扫描整个老年代这将会随着堆的增大变得越来越慢,特别是现在内存都越来越大了。所以为了提升性能就引入卡表。
卡表提升性能的原理:逻辑上把老年代内存分成一个个大小相等的卡页(card page,大小512byte),然后对每个卡片准备一个与其对应的标记位,并将这些位集中起管理就好像一个表格(mark table)一样,当改写对象引用是从老年代指向新生代时,在老年代对应的卡片标记位上设置标志位即可,通常这样的卡片我们称之为dirty card。这项操作可以通过上面的提到的write barrier来实现,这样就算对象跨多张卡片也不会有什么问题。卡表通常是用byte数组实现的,byte的值只能取[0,1]这两种。所以btye[i] = 1 就表示第i + 1 卡片所在内存上有指向新生代引用的老年代对象,这时只要tracing这个卡片上的对象即可。如果每个card大小的是128字节(1024位,)那卡表就只占整个老年代的1/1024之一。所以遍历卡表的时间会远比遍历整个老年代快得多!这其中背后思想就是典型以空间换时间的思路!这种思路在G1中也有体现,只不其对应的数据是remember set而已。
对于新生代:它记录老年代到新生代的引用,younggc时不用遍历整个老年代
对于老年代:它记录并发标记开始引用发生变化的card,并发标记结束后需要处理这些card
由于新生代GC与老年代GC同时使用card table,所以会出现冲突的情况
但这个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
通过参数CMSPrecleaningEnabled选择关闭该阶段,默认启用,主要做两件事情:
处理dirty card,降低remark阶段暂停时间。
重新标记的过程是STW的,所以为了缩短停顿时间,在并发标记之前应该尽可能多的完成重新标记阶段的工作。并发预清理就是对dirty card进行遍历处理,降低重新标记需要处理的dirty card的数量。
可中断预清理concurrent abortable preclean
该阶段发生的前提是,新生代Eden区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold 默认是2M,如果新生代的对象太少,就没有必要执行该阶段,直接执行重新标记阶段。
为什么需要这个阶段,存在的价值是什么?
因为CMS GC的终极目标是降低垃圾回收时的暂停时间,所以在该阶段要尽最大的努力去处理那些在并发阶段被应用线程更新的老年代对象,这样在暂停的重新标记阶段就可以少处理一些,暂停时间也会相应的降低。
在该阶段,主要循环的做两件事:
当然了,这个逻辑不会一直循环下去,打断这个循环的条件有三个:
重新标记final remark
在之前的并行阶段,可能产生新的引用关系如下:
上述对象中可能有一些已经在Precleaning阶段和AbortablePreclean阶段被处理过,但总存在没来得及处理的,所以需要做以下事情:
在第1步骤中,需要遍历新生代的全部对象,如果新生代的使用率很高,需要遍历处理的对象也很多,这对于这个阶段的总耗时来说,是个灾难(因为可能大量的对象是暂时存活的,而且这些对象也可能引用大量的老年代对象,造成很多应该回收的老年代对象而没有被回收,遍历递归的次数也增加不少),如果在这之前发生一次YGC,这样就可以避免扫描无效的对象。
CMS算法中提供了一个参数:CMSScavengeBeforeRemark,默认并没有开启,如果开启该参数,在执行该阶段之前,会强制触发一次YGC,可以减少新生代对象的遍历时间,回收的也更彻底一点。
并发清除concurrent sweep
并发清除标记为不可达的对象,回收并合并空闲内存。
并发重置concurrent reset
重新设置CMS相关的各种状态及数据结构,为下一个垃圾收集周期做好准备。
并发带来的好处是可以降低用户线程的停顿时间,对于在线服务类应用非常有益,因为长时间的停顿可能导致响应超时等问题。但相对于非并发垃圾收集器,CMS整个周期内很多工作是重复的(比如重新标记阶段对dirty card中的对象重新标记,而在并发标记阶段可能已经标记过了),导致整体的吞吐量是降低的。
浮动垃圾
内存碎片
CMS默认开启UseCMSCompactAtFullCollection 参数,在FullGC时进行内存碎片的合并整理。内存碎片虽然解决了,但负面影响就是停顿时间变长了。还有另外一个CMSFullGCsBeforeCompaction参数可以控制多少次FullGC才会进行整理,默认是0代表每次FullGC都会进行碎片整理。
运行过程常见问题
发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/215199.html原文链接:https://javaforall.net
