Incermental GC

增量式垃圾回收

增量式垃圾回收 Incremental GC

一种通过逐渐推进垃圾回收来控制mutator最大暂停时间的方法。

什么是增量式垃圾回收

有时候GC时间太长会导致mutator迟迟不能进行。如下图示:

这样的GC称为停止型GC(Stop the world GC)。

为此出现了增量式垃圾回收。增量(incremental)式垃圾回收是将GC和mutator一点点交替运行的手法,如下图示:

三色标记算法

描述增量式垃圾回收算法使用Edsger W.Dijkstra等人提出的三色标记算法(Tri-color marking)。将GC中的对象按照各自的情况分为三种,使用三种颜色代替。如下

  • 白色:还未搜索过的对象
  • 灰色:正在搜索的对象
  • 黑色:搜索完成的对象

以下使用GC标记-清除算法为示例:

GC开始运行前的所有对象都是白色,GC一旦开始运行,所有能从根到达的对象都会被标记,然后送到栈里。这样的对像是灰色。灰色的对象会从栈中取出,其子对象也会被涂成灰色,当所有子对象全部被涂成灰色,这时该对象就会成为黑色。

GC结束的时候,活动对象全部是黑色,垃圾为白色。

以上仅仅是举例说明,具体用什么标记它的颜色,只要能显示出三种状态就行。具体使用什么方法,自己选。

GC 标记清除算法的分割

GC标记清除算法那增量式运行的三个阶段

  • 根查找阶段:将根直接指向对象标记成灰色
  • 标记阶段:将子对象涂成灰色,结束时候所有对象都是黑色
  • 清除阶段:查找清除白色对象连接到空闲链表。将黑色对象变为白色对象。

下面是增量式垃圾回收的incremental_gc()函数

incremental_gc(){
    case $gc_phase  // 检查变量,判断应该进入那个阶段
    when GC_ROOT_SCAN  // 进入查找阶段
        root_scan_phase()
    
    when GC_MARK  // 进入标记阶段
        incremental_mark_phase()
    else
        incremental_sweep_phase() //清除阶段
    
}
  • 当进入根查找阶段,我们直接把根引用的对象打上记号,放入栈中。GC开始时运行一次。
  • 根查找结束后,incremental_gc()会告一段落,mutator会再次开始运行。
  • 下来再次执行incremental_gc(),函数进入标记阶段。在标记阶段incremental_mark_phase()函数会从栈中取出对象和搜索对象。操作一定次数后,mutator会再次开始运行。直到栈标记为空。
  • 之后就是清除阶段。incremental_sweep_phase()函数不是一次性清除整个堆,而是每次只清除一定个数,然后中断GC,再次运行mutator。

根查找阶段

根查找阶段非常简单。作为根查找实体的 root_scan_phase() 函数,如代码清单所示:

root_scan_phase(){
    for(r : $roots)
        mark(*r)
    $gc_phase = GC_MARK
}

对能直接从根找到的对象调用 mark() 函数。mark() 函数的伪代码如下所示。

mark(obj){
    if(obj.mark == FALSE)
        obj.mark = TRUE
        push(obj, $mark_stack) }

如果参数 obj 还没有被标记,那么就将其标记后堆到标记栈。这个函数正是把 obj 由白色涂成灰色的函数。

当我们把所有直接从根引用的对象涂成了灰色时,根查找阶段就结束了,mutator会继 续执行。此外,这时 $gc_phase 变成了 GC_MARK。也就是说,下一次 GC 时会进入标记阶段。

标记阶段

incremental_mark_phase(){
    for(i :1...MARK_MAX)
        if(is_empty($mark_stack) == FALSE) //从栈中取出对象,将其子对象涂成灰色。
            obj = pop($mark_stack)
            for(child :children(obj))
                mark(*child) // 递归涂子孩子。
        else
            for(r :$roots) // 再次对根直接引用的对象进行标记。因为第一次标记根本没有进行完,而且之后也可能发生变化。
                mark(*r)
            while(is_empty($mark_stack) == FALSE)
                obj = pop($mark_stack)
                for(child :children(obj))
                    mark(*child)
            
            $gc_phase = GC_SWEEP // 为清除阶段做准备
            $sweeping = $heap_start
            return
            
}
  • 可以看到首先从栈中取出对象,将其子对象涂成灰色。但是这一系列操作只执行了MARK_MAX次。我们知道增量式的垃圾回收不是一次性处理完了。所以这个MARK_MAX就显得格外重要了
  • 之后在标记即将结束前,对根对象指向的对象再次标记。原因如下图示:

我们可以看到由于增量式垃圾回收它是一步一步走的,并不是说一次就把GC做完,所以它在GC的过程中指针时会变化的。如果变化如上图,我们又不对其重新标记,那得到的结果就是,C对象被删掉了。很严重的一个后果啊。

为了防止这样,我们又一次的使用了写入屏障。

写入屏障

看一下Edsger W. Dijkstra 等人提出的写入屏障

write_barrier(obj, field, newobj){
    if(newobj.mark == FALSE)
        newobj.mark = TRUE
        push(newobj, $mark_stack)
    
    *field = newobj
}

如果新引用的对象newobj没有被标记过,就将其标记后堆到标记栈里。

即使在 mutator 更新指针后的图中c,也没有产生从黑色对象指向白色对象的引用。这样一来我们就成功地防止了标记遗漏。

清除阶段

当标记栈为空时,GC就会进入清除阶段。代码清单如下:

incremental_sweep_phase(){
    swept_count = 0
    while(swept_count < SWEEP_MAX)
        if($sweeping < $heap_end)
            if($sweeping.mark ==TRUE)
                $sweeping.mark = FALSE
            else
                $sweeping.next = $free_list
                $free_list = $sweeping
                $free_size += $sweeping.size
            $sweeping += $sweeping.size
            swept_count++
        else
        $gc_phase = GC_ROOT_SCAN
        return 
        
}

该函数所进行的操作就是把没被标记的对象连接到空闲链表,取消已标记的 对象的标志位。

为了只对一定数量的对象进行回收,事先准备swept_count用来记录数量。swept_count >= SWEEP_MAX 时,就暂停清除阶段,再次执行 mutator。当把堆全部清除完毕时,就将 $gc_phase 设为 GC_ROOT_SCAN,结束 GC

分配

newobj(size){
    if($free_size < HEAP_SIZE * GC_THRESHOLD) // 如果分块的总量 $free_size 少于一定的量HEAP_SIZE就执行GC
        incremental_gc()
    
    chunk = pickup_chunk(size, $free_list) // 搜索空闲链表返回大小时size的块
    if(chunk != NULL)
        chunk.size = size
        $free_size -= size
        if($gc_phase == GC_SWEEP && $sweeping <= chunk) // 判断GC是否在清除阶段和chunk是不是在已清除完毕的空间
            chunk.mark = TRUE  // 没有在清除完毕的空间,我们要设置标志位
        return chunk
    else
        allocation_fail()
}
  • 判断$free_size 是不是小于HEAP_SIZE * GC_THRESHOLD,如果是就执行GC。
  • 在空闲链表查找大小为size的分块,并返回。
  • 对分块进行标记,对$free_size进行后移操作。
  • 判断 GC状态,和chunk状态。
  • 如果chunk在清除完毕的空间的空间里什么都不做,如果不在则进行标记。

优点和缺点

缩短最大暂停时间

  • 增量式垃圾回收通过交替运行GC和mutator来减少停止时间,减少二者的相互影响。从而保证GC不会长时间妨碍mutator。
  • 增量式垃圾回收不是重视吞吐量,而是重视如何缩短最大暂停时间。

降低了吞吐量

  • 写入屏障会增加额外负担。但是这是必要的牺牲啊啊啊。
  • 高吞吐量和缩短最大暂停时间,二者不可兼得。根据需要选择最合适的最好。

Steele 的算法

1975,Guy.Steele

这个算法中使用的写入屏障条件更严格,它能减少GC中错误标记的对象。

mark()函数

mark(obj){
    if(obj.mark == FALSE)
        push(obj, $mark_stack)
}

再把对象放入标记栈的时候还没有标记,在这个算法中从标记栈取出时才为它设置标记为。这里的灰色对象时“标记栈里的没有设置标记位置的对象”,黑色是设置了标识位的对象。

写入屏障

write_barrier(obj, fieldm newobj){
    if($gc_phase == GC_MARK && obj.mark == TRUE && newobj.mark == FALSE)
        obj.mark = FALSE
        push(obj, $mark_stack)
        
    *field = newobj
}

  • 判断条件,条件成立时,将obj.mark设置为FALSE
  • 将obj 放入标记栈。
  • 如果标记过程中发出引用的对象时黑色,且新的引用对象为灰色或者白色,那么我们就把发出引用的对象涂成灰色。

如上图示:写入屏障在a到b中发挥了作用。对象A被涂成了灰色,其结果就是c中不存在从黑色对象指向的白色对象,也就不会出现把活动对象标记遗漏的状况了。

当A对象为灰色的时候,我们会再次对A对象进行搜索和标记。

汤浅的算法(不放入cnblog)

汤浅太一,1990 Snapshot GC

这种算法是以GC开始时对象间的引用关系(snapshot)为基础来执行GC的。因此,根据汤浅算法,在GC开始时回收垃圾,保留GC开始时的活动对象和GC执行过程中被分配的对象。

标记阶段

incremental_mark_phase(){
    for(i :1..MARK_MAX)
        if(is_empty($mark_stack) == FALSE)
            obj = pop($mark_stack)
            for(child: children(obj))
                mark(*child)
        else
            $gc_phase = GC_SWEEP
            $sweeping = $heap_start
            return
}
  • 在汤浅算法中,清除阶段没有必要再去搜索根了,因为该算法以GC开始时对象间的引用关系为基础执行GC。
  • 在标记阶段中,新的从根引用的对象在GC开始时应该会被别的对象锁引用。因此搜索GC开始时就存在的指针,就会发现这个对象已经被标记完毕了。所以没有必要从新的根重新标记它。

从黑色对象指向白色对象的指针

之前我们提到过,使用写入屏障来防止产生从黑色对象指向白色对象的指针。但是汤浅算法中我们允许黑色对象指向白色对象。这样还能回收成功的原因是因为GC一开始就保留活动对象的这项原则。

遵循这项原则,就没有必要在新生成指针时标记引用的目标子对象。即使生成了从黑色对象指向白色对象的指针,只要保留了GC开始时的指针,作为引用目标的白色对象早晚都会被标记。

其实指针被删除时的情况应该引起我们的注意。指向对象的指针删除,就可能无法保留GC开始时的活动对象了。因此在汤浅的写入屏障中,再删除指向对象的指针时要进行特殊处理。

写入屏障

write_barrier(obj, field, newobj){
    oldobj = *field
    if(gc_phase == GC_MARK && oldobj.mark == FALSE)
        oldobj.mark = TRUE
        push(oldobj, $mark_stack)
    *field = newobj
}

当GC进入到标记阶段且oldobj是白色对象,就将其涂成灰色。

图b转移到图c的过程中写入屏障发挥了作用,他把c涂成了灰色,这样就防止c的标记遗漏。

图b中,黑色对象指向了白色对象。但是B指向C并没有被删除。在汤浅的写入屏障中这时候不会进行特殊的处理。只有当B指向C的指针被删除的时候,C才会变为灰色。

分配

newobj(size){
    if($free_size < HEAP_SIZE * GC_THRESHOLD)
        incremental_gc()
    
    chunk = pickup_chunk(size, $free_list)
    if(chunk != NULL)
        chunk.size = size
        $free_size -= size
        if($gc_phase == GC_MARK)
            chunk.mark = TRUE
        else if($gc_phase == GC_SWEEP && $sweeping <= chunk)
            chunk.mark = TRUE
        return chunk
        else
            allocation_fail()
            
}

在标记阶段进行分配时会无条件设置obj的标志位。也就是说,会把obj涂成黑色。汤浅算法的写入屏障比较简单,所以保留了很多对象,无意间也保留了很多垃圾对象。

比较各个写入屏障

作者 A B C 时机 动作
Dijkstra 从a到b 将C涂成灰色
Steele 白或灰色 从a到b 将A恢复为灰色
汤浅 从b到c 将C涂成灰色
原文地址:https://www.cnblogs.com/Leon-The-Professional/p/10048678.html