增量式垃圾回收

增量式垃圾回收 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. HD-ACM算法专攻系列(13)——How Many Fibs?

    问题描述: 源码: import java.math.BigInteger; import java.util.*; public class Main { //主函数 public static v ...

  2. ing在写作中到底怎么用

    改关静留的作业中我还得改英文摘要.于是有个地方用了ing.顺便查了一下. http://www.yygrammar.com/Article/201408/3677.html -ing分词用作状语时有九 ...

  3. POJ 3273 Monthly Expense 【二分答案】

    题意:给出n天的花费,需要将这n天的花费分成m组,使得每份的和尽量小,求出这个最小的和 看题目看了好久不懂题意,最后还是看了题解 二分答案,上界为这n天花费的总和,下界为这n天里面花费最多的那一天 如 ...

  4. ActiveMQ学习笔记(1)----初识ActiveMQ

    1. 什么是ActiveMQ? ActiveMQ是Apache推出的,一款开源的,完全支持JMS1.1和j2ee1.4规范的JMS Provider实现的消息中间件(Message Oriented ...

  5. WCF客户端获取服务端异常[自定义异常]

    引言 经过不断的摸索,询问/调试,终于学会了关于WCF客户端与服务端之间异常的处理机制,在此来记录自己的成果,用于记录与分享给需要的伙伴们. 首先感谢[.NET技术群]里群主[轩]的大力帮助,如有需要 ...

  6. HDU 2295 Radar 重复覆盖 DLX

    题意: N个城市,M个雷达站,K个操作员,问雷达的半径至少为多大,才能覆盖所有城市.M个雷达中最多只能有K个同时工作. 思路: 二分雷达的半径,看每个雷达可以覆盖哪些城市,然后做重复覆盖,判断这个半径 ...

  7. 个人创业了,做了个网站和App,www.91tianwu.com

    大家好! 很久没有写文章了,响应党和政府号召,创业了.此文为了推广我的网站而写,希望有兴趣的看看. 我做了添物网,地址:http://www.91tianwu.com. 主要做购物导航推荐,目前集中在 ...

  8. android将String转化为MD5的方法+一些String经常使用的方法

    public class StringUtils { public static String MD5Encode(String origin) { String resultString = nul ...

  9. HTML5实战与剖析之媒体元素(3、媒体元素的事件及方法)

    HTML5中的媒体元素除了拥有非常多的属性之外,video标签和audio标签还能够出发非常多事件和方法. 这些方法监控着不同的属性的变化,这些变化有可能是媒体播放的结果,也可能是用户操作媒体的结果. ...

  10. CAP定理在分布式系统设计中的最新应用

    本文翻译自国外InfoQ和计算机杂志上一篇2012年旧文,本文就有关数据同步进行了讨论,特别关注业务事务的不变性与一致性如何在分布式系统中巧妙保证,探讨了长时间运行的事务的补偿机制.这些对分布式系统设 ...