Concurrent Mark Sweep(cms)垃圾回收器

        好长时间没写过博客了,突发奇想,开始写下最近几年的积累吧,先从Concurrent Mark Sweep(cms)开始,希望自己没有太懒吧,坚持写完吧,先介绍以下概念:

GC ROOT

这里我引用下RednaxelaFX的原话,所谓“GC roots”,或者说tracing GC的“根集合”,就是一组必须活跃的引用(重点)。
例如说,这些引用可能包括:

  • 所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用;换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。
  • VM的一些静态数据结构里指向GC堆里的对象的引用,例如说HotSpot VM里的Universe里有很多这样的引用。
  • JNI handles,包括global handles和local handles
  • (看情况)所有当前被加载的Java类
  • (看情况)Java类的引用类型静态变量
  • (看情况)Java类的运行时常量池里的引用类型常量(String或Class类型)
  • (看情况)String常量池(StringTable)里的引用

CARD TABLE

  • 基于卡表(Card Table)的设计,通常将堆空间划分为一系列2次幂大小的卡页(Card Page)。
  • 卡表(Card Table),用于标记卡页的状态,每个卡表项对应一个卡页。
  • HotSpot JVM的卡页(Card Page)大小为512字节,卡表(Card Table)被实现为一个简单的字节数组,即卡表的每个标记项 为 1个字节。
  • 当对一个对象引用进行写操作时(对象引用改变),写屏障逻辑将会标记对象所在的卡页为dirty。
  • OpenJDK/Oracle 1.6/1.7/1.8 JVM默认的卡标记简化逻辑如下:
CARD_TABLE [this address >> 9] = 0;
  • 首先,计算对象引用所在卡页的卡表索引号。将地址右移9位,相当于用地址除以512(2的9次方)。可以这么理解,假设卡表卡页的起始地址为0,那么卡表项0、1、2对应的卡页起始地址分别为0、512、1024(卡表项索引号乘以卡页512字节)。
  • 其次,通过卡表索引号,设置对应卡标识为dirty。

Mod Union Table

  • 当一个card跨代(年轻代依赖老年代)引用,年轻代gc需要扫描这些dirty card,看是否有跨代引用,也可能由于跨代引用不存在了,年轻代会擦除这个dirty card的状态,但是dirty card只有一份,年轻代gc和老年代gc都操作会产生误操作,所以有了Mod Union Table,结构和card table基本一致。
  • 介绍完GCROOT,然后说下CMS的过程:

1:初始标记(stop the word)

  •  初始标记做的事情是二件:
  • ①:遍历GCRoot可直达的老年代对象(图中红颜色字体的1)
  • ②:遍历新生代直达的老年代对象  (图中红颜色字体的2和3)
  • 从上面的图中也可以看出来,初始标记是做了部分年轻代GC的事情,这里显然是可以优化的,g1就优化了这个过程,每次老年代的GC发生在年轻代GC之后,这样就省去了trace年轻代的过程,后面的帖子我会对比cms和g1设计上的不同和优化。

2:并发标记

 

并发标记和其名字一样,并发执行,主要做二件事情:

  • ①:沿着初始标记的1,2,3对象,进行trace遍历(4,5),直到所有对象被遍历标记完全,(6,7,8,9,10)未被标记,将被回收。
  • ②:新生代晋升到老年代,直接在老年代分配的对象,还有老年代内部引用变化的对象,这些对象所在的card table被标记为dirty,也就是脏卡(dirty card)。
  • 为什么会有这个操作,下面我介绍一下三色标记法:
  • 白色:还没有搜索过的对象(白色对象会被当成垃圾对象)
  • 灰色:正在搜索的对象
  • 黑色:搜索完成的对象(不会当成垃圾对象,不会被 GC)

A.c = C;
B.c = null;
  • 如果灰色对象B下面的引用的白色对象c在并发阶段,成为了黑色对象A下面的引用,那么会发生什么事情?会产生漏标,c对象最终会被回收,这是非常可怕的事情,活着的对象被回收了,这是不能接受的,处理这种情况一般有二种方式:
  • ①:在对象引用发生变化之前记录对象引用关系,灰色(B)对象断开白色对象(C)的引用时记录,保证不会漏标。(写前屏障)(B.c = null)g1
  • ②:在对象引用发生变化之后记录对象引用关系,黑色(A)对象新增白色(C)对象是记录,保证了不会漏标。(写后屏障)(A.c = c)cms
  • cms采用的是写后屏障,增量更新(Incremental Update),修改的对象在Mod Union Table里被标记为Dirty card。

3:并发预清理

  • 通过参数CMSPrecleaningEnabled选择关闭该阶段,默认启用:
  • ①:扫描并发标记阶段,老年代新增加的对象,Mod Union Table的Dirty Card,重新标记那些在并发标记阶段新增加和引用被更新的对象。

4:并发可中断的预清理

  • CMS 有两个参数:CMSScheduleRemarkEdenSizeThresholdCMSScheduleRemarkEdenPenetration,默认值分别是2M、50%。eden空间使用超过2M时,启动可中断的并发预清理(CMS-concurrent-abortable-preclean),eden空间使用率达到50%中断预清理。CMSMaxAbortablePrecleanTime=5s和循环次数(默认是0,不限制)CMSMaxAbortablePrecleanLoops也能控制退出。
  • ①:处理并发标记阶段和并发预清理阶段,新生代survivor新增加的对象(对老年代有引用),Dirty Card 和 Mod Union Table,并发预清理和可中断的预清理这二个阶段,都是为了最终标记减少标记的数量,减少系统停顿的时间。
  • 执行可中断的预清理的前提?
  • eden空间使用超过2M时,启动可中断的并发预清理(CMS-concurrent-abortable-preclean),eden空间使用率达到50%中断预清理
  • 为什么会有这个阶段?
  • 其实这个阶段更多的作用是期望能够发生一次minor gc(ParNew gc),因为接下来的Final Remark阶段要扫描整个的新生代,为什么要扫描新生代?因为新生代的对象关系变化比较大,Dirty Card卡比较多,与其扫描新生代的Dirty Card,不如直接扫描整个年轻代,但是如果新生代太大,扫描起来太费时间,就会得不偿失,Final Remark(stop the world)的时间会很长,所以期望发生一次minor gc回收年轻代,但也仅仅是期望。下面切一段代码:
 while (!(should_abort_preclean() ||ConcurrentMarkSweepThread::should_terminate())) {workdone = preclean_work(CMSPrecleanRefLists2, CMSPrecleanSurvivors2);cumworkdone += workdone;loops++;// Voluntarily terminate abortable preclean phase if we have// been at it for too long.if ((CMSMaxAbortablePrecleanLoops != 0) &&loops >= CMSMaxAbortablePrecleanLoops) {if (PrintGCDetails) {gclog_or_tty->print(" CMS: abort preclean due to loops ");}break;}if (pa.wallclock_millis() > CMSMaxAbortablePrecleanTime) {if (PrintGCDetails) {gclog_or_tty->print(" CMS: abort preclean due to time ");}break;}// If we are doing little work each iteration, we should// take a short break.if (workdone < CMSAbortablePrecleanMinWorkPerIteration) {// Sleep for some time, waiting for work to accumulatestopTimer();cmsThread()->wait_on_cms_lock(CMSAbortablePrecleanWaitMillis);startTimer();waited++;}}
  • 可中断的预清理退出的条件有二个,循环的次数大于CMSMaxAbortablePrecleanLoops,或者执行的时间大于5000ms (CMSMaxAbortablePrecleanTime),如果在中断条件之前发生一次年轻代gc(minor gc),大家都高兴,发生不了的话,可以用这个参数(CMSScavengeBeforeRemark)强行开启。
  • 既然eden的使用率50%就会中断预清理,那么何时能够触发minor gc? 因为50%触发不了minor gc。
  • 如果对象增长很快,导致触发了minor gc,这个阶段就会退出,中断预清理。如果对象增长很慢,eden的使用率超过50%,为了避免eden继续增长,最终标记遍历年轻代的成本增加,这个阶段就退出,中断预清理。从上面来看,这个阶段针对的是对象增长特别快的情况,不适应所有场景。如果需要降低最终标记的时间,可以使用CMSScavengeBeforeRemark这个参数,CMSScavengeBeforeRemark会执行一次minor gc,但是这个参数不是万能的,具体能不能降低时间,需要比较minor gc花费的时间是否小于最终标记扫描整个年轻代的时间。

5:最终标记(stop the word)

  • 由于上一个过程也是并发的,不可能所有对象都能被标记到,这个阶段就stop the world,查缺补漏,包含上面几个过程的全部内容
  • ①:遍历年轻代作为根标记老年代对象包括modUnionTable。
  • 为什么遍历年轻代?这是因为年轻代的对象变化的非常快,与其遍历整个年轻代的dirty card,不如直接遍历整个年轻代的对象。
  • ②:遍历dirty card标记老年代对象。
  • 这个过程很多人说不是重复了么,实际上,gcroot trace的过程,遇到被标记的第一个元素,就会终止,上面的过程并不多余。

6:并发清理

  • 如图,6,7,8,9就被清理掉了,这个阶段是并发的,但是效率并不是特别高,由于是并行的,还会产生浮动垃圾,就是对象变成不可达了,但是标记已经结束了,没法在标记了,就产生了浮动垃圾,g1的时候就变成了stop the word了。

7:并发重置

  • 最后一个阶段,重置cms的数据结构

 

到这里基本上可以结束了,但是总得凑下字数吧,整个的gc过程我大概说一下:年轻代minor gc之后,对象晋升到老年代,老年代的对象越来越多之后,就会触及阈值,就会触发cms gc;如果这个时候新对象来到老年代,老年代没有足够空间(Concurrent Mode Failure),就会触发serial old 做担保的full gc,如果full gc之后仍没有空间,就会触发oom,下一篇我就会说一个案例,如何分析的。


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部