Sapphire算法:GC Without Stop the World(上)

Go的GC一致为人诟病,然而Go1.5据说大大优化了GC,具体可以见这篇文章http://www.oschina.net/translate/go-gc-solving-the-latency-problem-in-go-1-5

于是我打开了Go源代码,查看了Go GC相关代码,注释中说,Go现在使用的GC是一种不用停止世界的GC,基于Richard大师2001年的论文,我便翻译了这篇paper,试图去理解这种GC算法,论文比较长,准备分为三篇发布,以下是正文:


Sapphire: Copying GC Without Stopping the World

Richard L.Hudson Intel Corporation 2200 Mision College Blvd. Santa Clara, CA 95052-8119 Rick.Hudson@intel.com

J. Eliot B. Moss Dept. Of Computer Science Univ. Of Massachusetts Amherst, MA 01003-4610 mss@cs.umass.edu

翻译:InsZVA

摘要

很多同时进行的垃圾回收(GC)算法已经被设计出来,然而很少有被实行和评估的,尤其是为了Java程序设计语言的。我们设计的Sapphire是一种同时拷贝GC算法。Sapphire注重最小化任何给定的线程需要阻塞以支持控制器的时间数量。(译者注:像Go1.5以前那种长时间GC暂停,甚至可达300ms,严重影响服务程序)特别地,Sapphire致力于工作在存在大量线程运行于小到中规模共享内存的多核处理器的情况下。一个Sapphire要处理的具体问题是在线程栈被调整来复制对象时,不能停止所有的线程(用GC用语讲,新对象的flip)。

Sapphire拓展了先前的算法,而且是与复制回收(copying collection)最近的一种GC技术,应用线程主要observe对象旧的拷贝【13】。Sapphire的最关键创新在于:(1)在同一时刻flip一个线程的能力(也就是将线程中对对象的旧副本的引用换成对对象新副本的引用),而不是在同一时刻停止所有线程来flip他们;(2)避免使用读屏障。

1.概述

Sapphire是一种为类型安全、堆分配内存的语言设计的新的同时复制GC算法。它致力于最小化垃圾回收时应用线程阻塞的时间。Sapphire修改优先算法的一个进步是对线程的flip的提升。之前的算法包括了一个使得所有线程全部停止的步骤,这个步骤中,线程的栈改变了,而且栈中的指针从对象的旧副本重定义到新副本。在可能含有多个线程的系统里,我们预测这种暂停可能是不可接受的(译者注:预见了Go吗,233~然后你就被Go挖去做GC了)。Sapphire也避免了读屏障的使用。

Sapphire目标程序是使用多核处理器的服务器程序。这种程序可能会拥有大量的线程,每个线程处理一个网络会话(译者注:简直就是为Go而生),和相当大数量的共享状态在主存中。我们对避免在一些时间中可能被明显感受到的GC导致的明显暂停很感兴趣。我们打算做一个算法,规模为共享主存可行的处理器的数目。Sapphire不会导致内存一致性问题,比如额外的内存屏障问题等(详见附录E)。

我们将以如下方式组织这篇论文:这个概述部分以一些有用的定义做结束。第二部分是这份报告的核心,讨论了收集(collection),详细阐述了创新之处。第三部分考虑我们如何结合在第二部分中为了更清楚地表达而分离的不同部分。第四部分将Sapphire与优先工作联系起来。第五部分描述了我们的原型工具,测试结果和结论。多个附录考虑了对主要思想不是必须的更多问题。

内存区域:我们定义了多个内存区域。一个内存区域可能包含多个槽(slot,可能包含指针的内存位置)和非槽数据。1 我们假设内存区中的所有槽都可以被明确地发现。

·U - 堆(很可能被多个线程共享)上面的一个区域,这个区域的对象在一个特定的回收中是不被管制的。U代表Uncollected。为了方便起见,我们把所有非特定线程不包含对象的槽也包含进U

·C - 堆(很可能被多个线程共享)上面的一个区域,这个区域的对象在一个特定的回收中是被管制的。C代表CollectedC会在之后被分成:

— O - 旧空间:在回收开始的时候就存在的对象副本

— N - 新空间:在回收中幸存的对象的新副本

C只包含对象,也就是说它不像可能含有很多不在对象中的槽的U,它不含裸槽。

·S - 栈:每个线程有一个分离的栈,是每个线程独有的。S区域包含槽,但是却不包含对象,也就是说可能没有从堆对象到栈的指针。2为方便起见,我们将其他线程本地的槽,比如那些相当于机器寄存器的引用。

对象被回收或否(上文中的UC)是Sapphire主动选择的。比如可能用变量代空间。Sapphire可以“背”一些它的需要在一个世代的收集器的写屏障,换句话说,Sapphire需要寻找从UC的指针,以及一个类似记录集(set)的数据结构来保存扫描U的状态。

Sapphire和复制回收的一个区别是我们假设新的对象是被分配在U中,而不是在C中。注意,这一点帮助保证标记和复制的结束因为C是不会增长的。这个决定施加了写屏障给新创建的对象,因为Sapphire需要处理他们指向C中对象的槽。

Sapphire是一个纯粹的复制回收器,这是准确的,与保守的使用不确定的根相对。可能这是明显的,但是在Sapphire中,回收器和所有方法线程是同时运行的。

2.Sapphire算法

Sapphire分为两个主要的组工作。第一组称为标记和复制,(a)确定哪些O中的对象是可以从US区域的根槽达到的,以及(b)在N中建立可以达到的O中对象的副本。在标记和复制过程中,方法线程仅仅读取和更新O中的对象副本。ON中任意给定对象的副本被保持着松散的同步:方法线程对O中对象副本在两个同步点之间的任何改动将会在进入第二个同步点之前被传递到N中的副本。这种方法采取了JVM规范的内存同步法则【12】。这个点,对两个副本的更新不需要自动、同时地做。一旦我们处于方法线程可以同时监视ON中的副本的回收工作中,如果所有方法线程都在同步点,那么ON中的副本将会相互一致。我们称之为ON空间的动态一致。

第二组工作,称为Flip,涉及Filp SU中的指针,使得他们指向N空间而不是O空间。在Sapphire中,这个工作使用仅一个写屏障(也就是说不使用读屏障)。Sapphire允许没有flip的线程同时访问ON中的副本(使用一个读屏障),或者确保所有访问都是到O的对象(然后同时flip所有)。升级的不含读屏障的flip提供了同时访问ON中副本的可能性。这种可能性需要稍微紧密一些的同步化来升级两个副本。

这也会影响指针相等的比较(Java中的==),因为他必须能够回应ON中同一对象的副本的指针在Java语言层面上相等;这种相等比较类似于Brooks实现的eq5】。注意,重要(a)常量null没有必要做额外工作去比较(b)永远不要比较两个位相等的指针或者变量为null的。这里有一份==的伪代码(明显是内联的,而且对一个参数为0的情况进行过优化的);有个叫做flip-pointer-equal应对复杂的场景:

// Pointer Comparison

Pointer-equal(p, q) {

if (p == q) return true;

if (q == 0) return false;

if (p == 0) return false;

return flip-pointer-equal(p, q);

}

flip-pointer-equal调用的确包含了一个高效的读屏障;然而我们声称这是一个罕见操作。

2.1 标记和复制工作:达到动态一致

具体操作是:标记、分配以及复制。注意实际上很多这些操作都被结合并同时进行,这个一会儿会讲到。如果我们将其分开,算法的描述将会更清晰。

一种有用的方法去理解这些操作是根据三色标记法(例子在【11】)。在这些规则下,每个槽和对象被认为是黑色,代表标记且扫描过了,灰色,代表标记了但是没有必要扫描,或者白色,代表没有标记。对象中的槽与对象的颜色一样。一个简单的条件约束颜色:黑色的槽不会指向白色的对象。Sapphire处理S中的槽位灰色,因此他们可以包含指向任何颜色对象的指针。这说明储存一个引用在栈的槽里面不需要任何工作来施加颜色规则。共享内存(全局变量和堆对象)需要工作,用写屏障的形式。

最初我们认为所有存在的对象和槽都是白色的,当回收执行的时候,对象的颜色由白色变到灰色,再到黑色。在Sapphire中,黑色的对象是永远不会变回灰色并重新扫描的。标记工作的目的是给每个可以达到的C中的对象染上黑色。之后,任何不能达到的对象从标记开始一直会保持白色,而且回收器最后会重新声明他们。新创建的对象被认为是黑色。

除了动态一致的情况,标记和复制工作本质上是一个同时进行的标记算法(被复制标记过的对象紧跟随的)。因此,他可以轻易地扩展算法来对待弱引用和最终化的情况。由于他们并不是这篇论文的焦点,我们之后不会讨论他们。

标记工作:分为三个步骤:预标记,安装标记工作的写屏障,根标记,处理非栈的根,以及堆/栈标记,完成标记。预标记安装写屏障,在这里以类C伪代码表示4:

// Mark Phase Write Barrier

// this is only for pointer stores

// the update is *p = q

// the p slot may be in U or 0

// the q object may be in U or 0

mark-phase-barrier(p, q) {

*p = q;

mark-write-barrier(q);

}

mark-write-barrier(q) {

if (old(q) && !marked(q)) {

// old && !marked means “white”

enqueue-object(q);

// enqueue object for collector

// to mark later

} }

注意方法(译者注:线程)不会进行任何直接标记,而是将对象入队来让回收器去标记。认为入队的对象都是灰色将会非常有用;那么这个写屏障施加了没有黑色指向白色的规则。

为什么入队而不是直接让方法线程去标记呢?基本事实,我们将会把标记和复制结合,而且标记步骤将会参与分配新对象副本的空间。让方法线程去做这个分配会导致一个同步瓶颈。我们通过让回收器执行分配和复制来避免这个瓶颈。之后,每个方法将会有自己的队列,入队将不会有任何同步管理。当回收器扫描一个方法的栈,他也会通过传递给一个单个的回收器输入队列来清空这个方法的队列。

根标记步骤迭代U中的槽而且使用mark-write-barrier将所有被这些槽引用的白色的C对象染成灰色。我们认为这将会使U中的槽变黑。注意关于这个步骤,储存新分配的对象,包括初始化储存,都会牵扯到mark-write-barrier,相关的对象会立刻出现在回收器的输入队列中。

当它可以扫描U区域来寻找相关的槽,更可能的情况是它使用写屏障建立的记录集数据结构来更有效率地定位相关的槽。

在堆/栈标记步骤中,回收器从输入队列,一系列的灰色(被标记的)对象,和线程栈开始工作。对每个入队的对象,回收器检查此对象是否已经被标记过了。如果是的,回收器丢弃这个队列的入口;否则,标记这个对象并将其置入清楚的灰色集合用于扫描。对每个灰色集合中的对象,它的槽会被染黑(在这些槽的引用上使用mark-write-barrier),然后这个对象本身会被认为是黑色的。这是因为它被标记了,但是并没有在灰色集合中,所以我们认为它是黑色的。回收器将会这样重复地处理,直到输入队列和灰色栈都空。

注意一个对象可能会被同一个或多个线程入队以标记超过一次;然而,最终回收器将会标记它,它不再会被线程入队。

/栈标记也会涉及发现S中指向O中对象的指针。为了扫描一个方法线程的栈,收集器简单地停止方法线程在一个安全点(后面还会讲到),然后扫描这个线程的栈(以及寄存器)来寻找对O中白色对象的引用,对每个引用调用标记工作的写屏障。(它可能使用栈屏障来限制栈的扫描暂停。)当线程被停止的时候,回收器将线程中入队的对象移动到收集器的队列中(仅仅使用一些指针同时来更新队列)。回收器继续这个线程,然后处理他的队列,染灰直到这个队列也变空。

虽然扫描一个独立线程的栈中指向白色对象的指针是容易的,但是很难见到在所有线程栈中都找不到指向白色对象的指针的情况。关键问题是即使一个线程的栈被扫描过了,这个线程仍然能将很多白色的指针进入它的栈,因为没有读屏障来防止这种情况的发生。理解这个解决方法需要知道一个关键事实,线程是不能写入白色的堆对象引用,因为写屏障首先会毫无保留地染灰白色的引用。

假设我们在一个特定的时刻t1,以及之后的时刻t2之间扫面了每个线程的栈,没有任何线程有白色的指针,没有一个线程有入队的对象,收集器的队列和灰色集合一直是空的。我们称此时在S和标记过的O对象中没有白色的指针,然后标记工作已经完成。我们观察到,一个线程只能够一个(可达到的)灰色对象或者白色对象中获得白色指针。在t1t2之间没有任何对象是灰色的,所以一个线程仅仅能够从白色的对象中获得白色指针,而且这个线程必须已经拥有了这个对象的指针。但是如果这个线程拥有任何白色的指针,并且在回收器扫面它的栈的时候丢弃掉这些指针的话,他将从此无法再获得任何白色的指针。这将应用到所有的线程,那么他们的栈中将不会包含任何白色的指针。

这个主题讨论关于可以直接到达的O对象。O对象最开始被U中的槽指向,这些O对象全部被加入到灰色集合中,而且已经被处理了,从t1之后没有任何O对象被写屏障添加。一个黑色对象到达白色对象的链条一定经过了灰色的对象(因为三色原则),由于此时没有灰色的对象,所以所有能到达的O都被标记了。

以下是两点可能有用的对栈扫描的改进。首先,从上次扫描开始就一直挂起的线程,在此次扫描中没有必要重复扫描。5第二,如果我们使用了栈屏障【6】,我们可以避免重复扫描从我们上次扫描开始还没有被线程调用的旧框架。

由于将指针的储存和与之关联的写屏障分离的可能性和必要性,栈扫描要求线程处于GC一致状态,就是说每个堆储存的写屏障已经被执行。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值