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)

3:并发预清理

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

4:并发可中断的预清理

  • CMS 有两个参数:CMSScheduleRemarkEdenSizeThresholdCMSScheduleRemarkEdenPenetration,默认值分别是2M、50%。两个参数组合起来的意思是预清理后,eden空间使用超过2M时启动可中断的并发预清理(CMS-concurrent-abortable-preclean),直到eden空间使用率达到50%时中断,进入remark阶段。CMSMaxAbortablePrecleanTime=5s和循环次数(默认是0,不限制)CMSMaxAbortablePrecleanLoops也能控制退出。
  • ①:处理新生代(survivor不是eden)引用到的老年代的对象,modUnionTable等
  • 为什么会有这个阶段?
  • 其实这个阶段更多的作用是期望能够发生一次minor gc(ParNew gc),因为接下来的Final Remark阶段要扫描整个的新生代,为什么要扫描新生代?因为新生代的对象关系变化比较大,Dirty Card卡比较多,与其扫描新生代的Dirty Card,不如直接扫描整个年轻代,但是如果新生代太大,扫描起来太费时间,就会得不偿失,Final Remark(stop the world)的时间会很长,所以期望发生一次minor gc回收年轻代,但也仅仅是期望,很多人认为CMSScheduleRemarkEdenPenetration=50%;永远也到不了100%,不可能触发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 accumulate
        stopTimer();
        cmsThread()->wait_on_cms_lock(CMSAbortablePrecleanWaitMillis);
        startTimer();
        waited++;
      }
    }
  • 因为preclean_work标记需要时间,如果你的对象增长很快,还没有来得及下个while开始,对象已经填满eden区,就会发生minor gc,如果这个时间刚好是CMSMaxAbortablePrecleanTime=5s,那么大家都高兴,但是有时候事实和我们有误差,用的时间还差5秒很多,这时候eden区的内存增长到50%退出,这个50%是为了避免二次minor gc,长时间的停顿,所以我们的minor gc实际上这种就没有太大的帮助,所以这二个参数CMSMaxAbortablePrecleanTime和CMSScheduleRemarkEdenPenetration是可以优化,我们也可以加上CMSScavengeBeforeRemark这个参数进行final remark前的优化,进行一次minor gc,出现这种情况毕竟计算机硬件发展太快,以前的默认参数,不一定符合现在的情况,jdk5出来已经很多年了。

5:最终标记(stop the word)

  • 由于上一个过程也是并发的,不可能所有对象都能被标记到,这个阶段就stop the world,查缺补漏,包含上面几个过程的全部内容
  • ①:遍历年轻代作为根标记老年代对象包括modUnionTable。
  • ②:遍历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,下一篇我就会说一个案例,如何分析的。

猜你喜欢

转载自blog.csdn.net/u014274324/article/details/106939714
今日推荐