增量式垃圾回收

增量式垃圾回收 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涂成灰色

Incermental GC的更多相关文章

  1. 46张PPT讲述JVM体系结构、GC算法和调优

    本PPT从JVM体系结构概述.GC算法.Hotspot内存管理.Hotspot垃圾回收器.调优和监控工具六大方面进行讲述.(内嵌iframe,建议使用电脑浏览) 好东西当然要分享,PPT已上传可供下载 ...

  2. C++随笔:.NET CoreCLR之GC探索(4)

    今天继续来 带大家讲解CoreCLR之GC,首先我们继续看这个GCSample,这篇文章是上一篇文章的继续,如果有不清楚的,还请翻到我写的上一篇随笔.下面我们继续: // Initialize fre ...

  3. C++随笔:.NET CoreCLR之GC探索(3)

    有几天没写GC相关的文章了哈,今天我讲GC的方式是通过一个小的Sample来讲解,这个小的示例代码只有全部Build成功了才会有.地址为D:\coreclr2\coreclr\bin\obj\Wind ...

  4. C++随笔:.NET CoreCLR之GC探索(2)

    首先谢谢 @dudu 和 @张善友 这2位大神能订阅我,本来在写这个系列以前,我一直对写一些核心而且底层的知识持怀疑态度,我为什么持怀疑态度呢?因为一般写高层语言的人99%都不会碰底层,其实说句实话, ...

  5. C++随笔:.NET CoreCLR之GC探索(1)

    一直是.NET程序员,但是.NET的核心其实还是C++,所以我准备花 一点时间来研究CoreCLR和CoreFX.希望这个系列的文章能给大家带来 帮助. GC的代码有很多很多,而且结构层次对于一个初学 ...

  6. Jvm 内存浅析 及 GC个人学习总结

    从诞生至今,20多年过去,Java至今仍是使用最为广泛的语言.这仰赖于Java提供的各种技术和特性,让开发人员能优雅的编写高效的程序.今天我们就来说说Java的一项基本但非常重要的技术内存管理 了解C ...

  7. Mono 3.2.7发布,JIT和GC进一步改进

    Mono 3.2.7已经发布,带来了很多新特性,如改进的JIT.新的面向LINQ的解释器以及使用了64位原生指令等等. 这是一次主要特性发布,累积了大约5个月的开发工作.看上去大部分改进都是底层的性能 ...

  8. 深入浅出聊优化:从Draw Calls到GC

    前言: 刚开始写这篇文章的时候选了一个很土的题目...<Unity3D优化全解析>.因为这是一篇临时起意才写的文章,而且陈述的都是既有的事实,因而给自己“文(dou)学(bi)”加工留下的 ...

  9. 不得不知的CLR中的GC

    引言 GC 作为CLR的垃圾回收器,让程序员可以把更多的关注度放在业务上而不是垃圾回收(内存回收)上.其实很多语言也有类似的东东, 如Java也有JIT 等等 GC基本概念 垃圾回收机制的算法有好多种 ...

随机推荐

  1. 51nod 1272 最大距离 O(nlog(n)) , 快排 , 最大连续子串

    题目: 解法:排序,把值小的和索引小的放在前面,记录一下之前索引最小的就可以了. 没什么可以讲的,上代码吧: #include <bits\stdc++.h> using namespac ...

  2. 安装Windows服务方法

    用sc create 服务名 binPath="路径",不要用老方法InstallUtil会出现一堆的错误

  3. rem 使用

    html{ font-size:12px; } .btn { width: 6rem; height: 3rem; line-height: 3rem; font-size: 2rem; displa ...

  4. div控制最小高度又自适高度

    div控制最小高度又自适高度我们在用div布局的时候经常会遇到这样的一种情况:我们需要设置一个div的高度,当里面的东西超过这个高度时,让这个容器自动被撑开,也就是自适应高度.当里面的信息很少时候,我 ...

  5. 基本数据类型(list、tuple)

    1.列表 1.1 定义 li=[1,2,3] 每个元素逗号隔开 list("abc") 迭代 列表是一个容器 => 任意类型 列表是有序的 => 索引 切片 步长 列表 ...

  6. HDU-1052 Tian Ji -- The Horse Racing 贪心 考虑特殊位置(首尾元素)的讨论

    题目链接:https://cn.vjudge.net/problem/HDU-1052 题意 田忌赛马问题扩展版 给n匹马,马的能力可以相同 问得分最大多少 思路 贪心做得还是太少,一开始一点思虑都没 ...

  7. Adobe AIR and Flex - 保存序列化对象文件(译)

    创建任何桌面应用程序几乎总是需要在本地存储数据,通过Adobe AIR我们有几下面几个选择,一个是我们能够使用内置的 SQLite 数据库支持,对于少量的数据这是大材小用了.另外一个选择是我们通过把数 ...

  8. HDU——T 1150 Machine Schedule

    http://acm.hdu.edu.cn/showproblem.php?pid=1150 Time Limit: 2000/1000 MS (Java/Others)    Memory Limi ...

  9. Cannot set web app root system property when WAR file is not expanded

    Cannot set web app root system property when WAR file is not expanded 在tomcat下面可以,在weblogic下面不行的处理方法 ...

  10. Android里使用正則表達式

    在Android里怎样使用正則表達式: 以验证username为例.username一般字母开头,同意字母数字下划线.5-16个字节: String regEx = "^[a-zA-Z][a ...