再回首CMS垃圾回收

前言

以前学习JVM垃圾回收时,主要是过了一遍垃圾收集算法,好比复制算法,标记-清除算法,标记-整理算法,在此基础上能够增长分代,每代采起不一样的回收算法,以提升总体的分配和回收效率。而后过了一遍JVM中的垃圾收集器,好比Serial、Parallel Scavenge、Parallel New、CMS、G1等。java

自认为垃圾收集就是根据GC Root标记全部可达的对象,而后把全部没有标记的对象清除就ok了。是否是很简单。事实上垃圾收集也就是这么一回事,可是不少时候提及来简单,作起来却会出现不少问题。这篇文章就是记录我对CMS垃圾收集器的一些疑问并学习的过程。算法

首先看一下CMS的总体流程(具体每一个流程的详情就自行了解吧)数组

CMS流程

如何进行标记?

最近在看Golang的GC算法实现,里面用到了三色标记法,可是在个人知识库中对三色标记法有这个概念,是的,我只知道这个概念,不知道三色标记法是怎么一个流程,也不知道三色标记法在GC中怎么与运行的。因而就开始了个人探险之旅。markdown

在搜索了一下三色标记法(具体能够看一下文末参考文档中三色标记法与读写屏障了解详情)后,发现现代追踪式(可达性分析)的垃圾回收器几乎都借鉴了三色标记的算法思想,CMS垃圾收集器也不例外。数据结构

GC Root有哪些?

咱们知道怎么进行标记了,但最初标记的时候须要一些根据才行啊,这些根据就是咱们收的GC Root。GC Root有哪些?网上有不少的答案,个人理解就是并发

  • 当前活跃调用栈中的指向对象的引用
  • 一些不会发生改变的数据所指向的引用

这里我使用的是引用,而不是对象,由于R大是这样说的(具体的问题见参考文档java的gc为何要分代?oop

所谓“GC roots”,或者说tracing GC的“根集合”,就是一组必须活跃的引用。 例如说,这些引用可能包括:学习

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

注意,是一组必须活跃的引用,不是对象。spa

如今知道了GC Root,可是咱们都知道有分代的概念,新生代的gc和老年的代的gc回收的区域是不同,那么这里的GC Root是否是应该不同呢?确定是不同的。线程

首先看一下新生代的GC

新生代的区域通常都比较小,并且对象的存活率都比较低,因此按照前面说的GC Root在新生代的区域扫描就好了。可是会有一个问题?老年代存在引用新生代对象的可能啊?若是只扫描新生代的区域,会漏掉被老年代引用的对象,这些对象就会被清除掉,这是不容许的。

若是这样的话,那是否是扫描一下老年代的对象,看是否引用新生代的对象是否是就ok了?嗯这么作确定是ok的,可是老年代通常很大,并且存活的对象不少,会致使扫描占用很长的时间。那这个问题如何解?JVM是如何避免Minor GC时扫描全堆的?

通过统计信息显示,老年代持有新生代对象引用的状况不足1%,根据这一特性JVM引入了卡表(card table)来实现这一目的。以下图所示:

CardTable.png

CardTable

卡表的具体策略是将老年代的空间分红大小为512B的若干张卡(card)。卡表自己是单字节数组,数组中的每一个元素对应着一张卡,当发生老年代引用新生代时,虚拟机将该卡对应的卡表元素设置为适当的值。如上图所示,卡表3被标记为脏(卡表还有另外的做用,标识并发标记阶段哪些块被修改过),以后Minor GC时经过扫描卡表就能够很快的识别哪些卡中存在老年代指向新生代的引用。这样虚拟机经过空间换时间的方式,避免了全堆扫描。

因此新年代GC的GC Root包含2部分

  • 新生代中知足GC Root定义的对象
  • 卡表中老年代引用新生代的对象

老年代的GC

前面咱们说了新生代的gc,咱们已一样的思路来看看老年代的gc,老年代的GC Root如何来标记呢?只扫描老年代能够吗?固然是不行的,由于新生代中也可能存在老年代对象的引用,好在新生代并不大,因此老年代GC的时候还须要扫描一遍新生代。

新生代GC的Root.png

因此老年代GC的GC Root包含2部分

  • 老生代中知足GC Root定义的对象,如图节点1;
  • 标记年轻代中活着的对象引用到的老年代的对象(指的是年轻代中还存活的引用类型对象,引用指向老年代中的对象)如图节点二、3;

并发标记的好坏?

标记做为垃圾回收的第一步,如今知道如何进行标记,接下来就是遍历这些对象,将全部未标记的对象清理就完成GC了。

然而事实上并无这么简单,若是标记的时候是STW的,那就是这么简单,可是若是标记过程都STW会形成暂停时间过长,给人的感受就是系统一卡一卡的。

因而就把标记的过程改为并发的进行,也就是CMS中并发标记的过程,然而这就是一切复杂问题的源头。虽然并发标记提高了标记的效率,可是所以却引起了一系列的问题。

由于并发标记时,gc线程和用户线程是并行的,因此在这个过程当中会出现下面的状况(须要了解三色标记法与读写屏障):

  • 新生代晋升到老年代
  • 黑色对象取消对灰色对象的引用(浮动垃圾)
  • 黑色对象新增对白色对象的引用(漏标)

其实在三色标记法与读写屏障文中已经给出了解决方法--添加读写屏障

  • 写屏障 + SATB
  • 写屏障 + 增量更新
  • 读屏障(Load Barrier)

在CMS并发标记阶段,使用 写屏障 + 增量更新 的方法,将上面出现的状况标记为dirty,这样最后再遍历处理一下Dirty集合中的对象就ok了

标记为dirty

从新标记阶段为何还要扫描新生代?

由于存在跨代引用,可是前面说过这种状况,经过读写屏障的方式标记这些为dirty,只须要扫描老年代和dirty集合就好了啊?哎,看来我仍是太年轻,若是只扫描老年代和dirty集合会漏掉一部分,会是哪部分呢?老年代和dirty集合尚未覆盖完吗?

是的,老年代和dirty集合的确没有覆盖完。咱们来分析一下。老年代中通过初始标记和并发标记后,只有黑色对象和白色对象了,黑色的就是要留下的,白色的就是要被清除的。黑色对象是怎么来的?根据GC Root找到的,因此只要并发标记过程当中,GC Root不发生变化,黑色对象就没有问题(不会漏标),若是在并发标记过程当中GC Root发生了变化呢?

当并发标记过程当中GC Root增长了,而且这个GC Root还引用了老年代中的对象,此时若是只扫描老年代和dirty集合就会漏标。所以从新标记阶段仍然须要扫描新生代。

预处理阶段都干了啥?

预处理阶段其实有2部分:

  • 预清理阶段
  • 可终止的预处理

这个阶段的目的都是为了减轻后面的从新标记的压力,提早作一点从新标记阶段的工做。通常CMS的GC耗时80%都在remark阶段,因此预处理阶段也是为了减小remark阶段的STW时间。

从新标记阶段须要作一下工做:

  1. 遍历新生代对象,从新标记
  2. 根据GC Roots,从新标记
  3. 遍历老年代的Dirty Card,从新标记(这里的Dirty Card大部分已经在clean阶段处理过)

遍历新生代对象时,可能不少对象已是不可达了,可是仍是须要扫描。遍历Dirty Card作处理。

这2部分其实就是预处理阶段帮助从新标记减轻压力的地方

  • 预清理阶段和可终止的预处理都会扫描Dirty Card作处理
  • 可终止的预处理,尽可能进行一次ygc,让不可达的对象被回收掉,remark阶段遍历新生代的对象成本小一点

具体这个阶段的详情见参考文档图解CMS垃圾回收机制,你值得拥有

参考文档

相关文章
相关标签/搜索