JVM-G1算法和数据结构那些事

本文来自OPPO互联网技术团队,转载请注名做者。同时欢迎关注咱们的公众号:OPPO_tech,与你分享OPPO前沿互联网技术及活动。html

人的状况和树相同。它愈想开向高处和明亮处,它的根愈要向下,向泥土,向黑暗处,向深处,向恶——千万不要忘记。咱们飞翔得越高,咱们在那些不能飞翔的人眼中的形象越是眇小。

——尼采《查拉图斯特拉如是说》java

每每,最基础最底层的知识里,蕴含着原始而强大的力量。本文将以java8 SE HotSpot VM为依据,盘点G1里的算法和数据结构。算法

本文将讲述:数组

  • 三色标记法:解释了为何并发类回收器须要从新标记和stw的过程;
  • card table&remember set:解释了G1为何能够在GC时不用扫描整个年老代从而对大堆更友好;
  • satb:解释了G1如何处理并发标记过程当中的新增对象和引用变动以及浮动垃圾从何而来;
  • collection sets:简要说明为何G1能够实现回收时间大体可控。

三色标记法

并发类回收器的并发标记阶段,gc线程和应用线程是并发执行的。因此一个对象被标记以后,应用线程可能篡改对象的引用关系,从而形成对象的漏标、误标。数据结构

其实误标没什么关系,顶多形成浮动垃圾,在下次gc仍是能够回收的。可是漏标的后果是致命的,把本应该存活的对象给回收了,从而影响的程序的正确性。并发

为了解决在并发标记过程当中,存活对象漏标的状况,GC HandBook把对象分红三种颜色:oracle

  • 黑色:根对象,或者该对象与它的子对象都被扫描
  • 灰色:对象自己被扫描,但还没扫描完该对象中的子对象
  • 白色:未被扫描对象,扫描完成全部对象以后,最终为白色的为不可达对象,即垃圾对象。

当GC开始扫描对象时,按照以下图步骤进行对象的扫描:ide

根对象被置为黑色,子对象被置为灰色;oop

继续由灰色遍历,将已扫描了子对象的对象置为黑色;post

遍历了全部可达的对象后,全部可达的对象都变成了黑色;不可达的对象即为白色,须要被清理。

这看起来彷佛很美好,然而--若是在标记过程当中,应用程序也在运行,那么对象的指针就有可能改变。这样的话,咱们就会遇到一个问题:对象丢失问题

咱们看下面一种状况,当垃圾收集器扫描到下面状况时:

这时候应用程序执行了如下操做:

A.c=C

B.c=null

这样,对象的状态图变成以下情形:

很显然,此时C是白色,被认为是垃圾须要清理掉,显然这是不合理的。

一个已经被灰对象指向白对象,在并发标记阶段会被漏标的充分必要条件是:

  • Mutator 插入了一个 黑对象 到 该白对象的引用;
  • Mutator 删除了全部 灰对象 到 该白对象的引用。

上面两个充要条件,只要打破一个就不会被漏标。就有以下两种可行的方式解决漏标:

  • 在插入的时候记录对象;
  • 在删除的时候记录对象。

恰好这对应CMS和G1的2种不一样实现方式——

1、CMS采用的是增量更新(Incremental update)(post-write barrier),致力于第一个条件的打破,只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的,即插入的时候记录下来。哪怕删除全部灰对象到该白对象的引用,remark 阶段从新回顾一次就不会漏标了

2、G1中,使用的是STAB(snapshot-at-the-beginning)(pre-write barrier)的方式,致力于第二个条件的打破。采用最保守的作法,把变动前的引用对象记录下来,看成是存活对象,让其活过这一周期。

[NextTAMS,top]指针之间的对象是并发标记期间新增对象,也在这一个周期里隐式存活。

所以 G1 的 SATB 会产生更多的浮动垃圾。可是换来的好处就是:不须要像 CMS 那样 remark,再走一遍 root trace 这种至关耗时的流程。

它有三个步骤:

这样,G1到如今能够知道哪些老的分区可回收垃圾最多。当全局并发标记完成后,就开始Mix GC。

Card Table&Remembered Set

年老代在堆中的比例远大于年轻代,年轻代比较小即便全堆扫描也成本不高,但若是每次YGC或Mixed GC都要扫描一次年老代全堆的话,确定会很是耗时,那么有什么好的解决方案呢?

JVM G1回收器使用了card table和remember set两个结构处理年老代全堆扫描的问题。

Card Table

基于卡表(Card Table)的设计,一般将堆空间划分为一系列2次幂大小的卡页(Card Page),HotSpot JVM的卡页大小为512字节。

卡表维护着全部的Card Page。Card Table的结构是一个字节数组,每一个卡表项映射着一个Card Page,为1个字节,用于标记卡页的状态。

Card Page中对象的引用发生改变时,写屏障逻辑将会把Card Page在Card Table数组中对应的值标记为dirty,就称这个Card Page被脏化了。

因此Card Table其实就是映射着内存中的对象,在进行Young GC的时候,即可以不用扫描整个老年代。

而是在卡表中寻找脏卡,并将脏卡中的对象加入到Minor GC的GC Roots里。当完成全部脏卡的扫描以后,全部脏卡的标识位清零。

对于一些热点Card Page会存放到Hot Card Cache中。同Card Table同样,Hot Card Cache也是全局的结构。

OpenJDK/Oracle 1.6/1.7/1.8 JVM默认的卡标记简化逻辑以下:

CARD_TABLE [this address >> 9] = 0;

首先:计算对象引用所在卡页的卡表索引号。将卡页地址右移9位,至关于用地址除以512(2的9次方)。能够这么理解:假设卡表卡页的起始地址为0,那么卡表项0、一、2对应的卡页起始地址分别为0、5十二、1024(卡表项索引号乘以卡页512字节)。

其次:经过卡表索引号,设置对应卡标识为dirty。

Card table随着堆内存一块儿初始化,全局惟一,经过具体的垃圾收集策略进行建立。

Remembered Set

每个Region都有本身的RSet。

虚拟机发现程序在对Reference类型的数据进行写操做时,会产生一个Write Barrier暂时中断写操做,检查Reference引用的对象是否处于不一样的Region之中。若是是,便经过Card Table把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。

维系RSet中的引用关系靠post-write barrier&Concurrent refinement threads来维护,操做伪代码以下:

void oop_field_store(oop* field, oop new_value) {
// pre-write barrier: for maintaining SATB invariant
// the actual store
  pre_write_barrier(field);  
*field = new_value;   
// post-write barrier: for tracking cross-region reference
  post_write_barrier(field, new_value);
}

RSet里面记录了引用--就是其余Region中指向本Region中全部对象的全部引用,也就是谁引用了个人对象。

RSet实际上是一个Hash Table,Key是其余的Region的起始地址,Value是一个集合,里面的元素是Card Table 数组中的index,既Card对应的Index,映射到对象的Card地址。

好比A对象在regionA,B对象在regionB,且B.f = A,则在regionA的RSet中须要记录一对键值对,key是regionB的起始地址,Value的值能映射到B所在的Card的地址,因此要查找B对象,就能够经过RSet中记录的卡片来查找该对象。

本分区对象引用本分区本身的对象,这种引用不用落入RSet中;同时,G1 GC每次都会对年轻代进行总体收集。

所以young->old和young->young也不须要在RSet中记录。而对于old->young和old->old的跨代对象引用,须要拥有RSet。

对于G1进行YGC时的跨代引用,以及进行Mixed GC时的old 间跨region引用,只要到本Region RSet所记录的region中扫描引用了脏card区域的对象,再如法溯源,判断其是否存活存活,进而肯定本分区内的对象存活状况。而不须要扫描整个堆了。

为了防止RSet溢出,对于一些比较热点的RSet会经过存储粒度级别来控制。

RSet有三种粒度—Sparse (稀疏) &Fine (细) &Coarse (粗)

对于热点RSet在存储时,根据细粒度的存储阀值,可能会采起粗粒度。这三种粒度的RSet都是经过Per Region Table来维护内部数据的。一个Per-Region-Table (PRT)是RSet存储颗粒度级别一个抽象。

Sparse PRT是一个包含Card目录的Hash Table:G1收集器内部维护这些Card。Card包含来自Region的引用,这个Region的引用是Card到Owning Region的关联的地址。

Fine-Grain PRT是一个开放的Hash Table:每个Entry表明一个指向Owning Region的引用的Region,Region里面的Card目录,是一个Bitmap。

当达到Fine-Grain PRT的最大容量,Coarse Grain Bitmap里面的相应的Coarse-Grained bit被设置,相应地Entry从Fine-Grain PRT删除。

Coarse bitmap有一个每一个Region对应的bit。Coarse Grain map设置bit意味着关联的Region包含到Owning Region的引用。

SATB

SATB(Snapshot-At-The-Begin)之因此叫这个名字,就是在初始标记开始时,G1 收集器打了一个快照,造成一个所谓的对象图(Object Graph)。

这个对象图记录在 next marking bitmap 之中 ,在并发标记阶段会在这个 bitmap 中 记录对象存活标记。最终Remark阶段结束后,完成对快照对象图全部标记。

图中能够很明确看到两个bitmap数据结构——G1 是借助 bitmap 来存放对象存活标记。每个 bit 表示每一个region中的某个对象起始地址,若是 bit 标记为 1(黑色),则表示该对象存活,bit 与对象对应有一套算法:

  • Bottom 指向 Region起点位置;
  • Top 永远指向当前Region 最新分配的对象,记录其起始位置;
  • PrevTAMS 和 NextTAMS 分别标记先后两次并发标记周期开始时,Top 指针的位置(TAMS - top at mark start);
  • End 表示 Region 终点位置。

[Bottom,PrevTAMS)-> 这部分的存活信息会在previous marking bitmap体现;

[Bottom, NextTAMS)-> 当清理时,PrevTAMS指向NextTAMS地址,NextTAMS归零,全部垃圾对象能经过[ Bottom, previousTAMS ]之间的对象快照被识别出来;

[NextTAMS, Top)-> 这部分对象在第 n 轮全局标记周期是隐式存活,SATB可以确保这部分的对象都会被标记,保障并发标记期间新增的对象不会被清理;

SATB利用pre-write barrier,将全部即将被修改引用关系的白对象旧引用记录下来,最后以这些旧引用为根从新扫描一遍,以解决白对象引用被修改产生的漏标问题。在引用关系被修改以前,插入一层pre-write barrier,代码以下:

pre-write barrier最终执行逻辑:

//openjdk/hotspot/src/share/vm/gc_implementation/g1/g1SATBCardTableModRefBS.cpp lines 52 ~ 65
void G1SATBCardTableModRefBS::enqueue(oop pre_val) {
    // Nulls should have been already filtered.
    assert(pre_val->is_oop(true), "Error");
    if (!JavaThread::satb_mark_queue_set().is_active()) return;
    Thread* thr = Thread::current();
    if (thr->is_Java_thread()) {
        JavaThread* jt = (JavaThread*)thr;
        jt->satb_mark_queue().enqueue(pre_val);
    } else {
        MutexLocker x(Shared_SATB_Q_lock);
       JavaThread::satb_mark_queue_set().shared_satb_queue()->enqueue(pre_val);
}
}

经过

G1SATBCardTableModRefBS::enqueue(oop pre_val)

把原引用保存到satb_mark_queue中,和RSet的实现相似,每一个应用线程都自带一个satb_mark_queue。在下一次的并发标记阶段,会依次处理satb_mark_queue中的对象,确保这部分对象在本轮GC是存活的。

然而,SATB也是有反作用的。若是被修改引用的白对象就是要被收集的垃圾,此次的标记会让它躲过GC,这就是float garbage。由于SATB的作法精度比较低,因此形成的float garbage也会比较多。

Collection Sets

Collect Set (CSet)是指:在Evacuation阶段,由G1垃圾回收器选择的待回收的Region集合。G1垃圾回收器的软实时的特性就是经过CSet的选择来实现的。

对于年轻代收集:CSet只容纳Eden Regions、Survivor Region;

对于混合收集:CSet还会容纳1/8的老年代Region。

G1将调整young的Region的数量来匹配软实时的目标;old region的选择将依据在Marking cycle phase中对存活对象的计数,G1选择存活对象最少的Region进行回收。

回收后CSet全部分区都会被释放,内部存活的对象都会被转移到分配的空闲Region中。

最后

成文时间所限,不免校对勘误疏漏,诸位看官还请宽容本人的愚钝,欢迎补充指正。

参考文献:

  1. https://docs.oracle.com/javas...
  2. https://docs.oracle.com/javas...
  3. https://www.jishuwen.com/d/2M...
  4. https://docs.cloudera.com/HDP...
  5. https://storage/content/recom...

相关文章
相关标签/搜索