Garbage Collection | 节点复制算法

来源:互联网 发布:纯css源码 编辑:程序博客网 时间:2024/06/15 02:45

节点复制算法


这次考察的也是一种基于追踪的算法:节点复制算法。节点复制式收集器将整个堆等分成为两个半区(semi-space),一个包含现有的数据,另一个包含已被废弃的数据,节点复制式垃圾收集从filp两个半区的角色开始。然后收集器在老的半区,也就是Fromspace中遍历存活的苏话剧结构,在第一次访问某个单元时把它复制到新的半区,也就是Tospace中去。在Fromspace中所有存活单元都被访问过之后,收集器在Tospace中建立了一个存活数据结构的副本,用户程序可以重新开始运行了,由于垃圾单元只是简单地被废弃在Fromspace中,人们常常把节点复制式收集器描述为“清道夫”:它们从垃圾中捡起有价值的对象并把对象带走。

节点复垃圾收集天生就具有一个有益的副作用:u哦有存活的数据结构都缩并地拍额在Tospace的底部。与那些存在内存碎片问题的收集器相比,缩并的收集器能能否更高效的分配对象。New过程只需要检查有没有足够的空间,然后在递增指向自由空间开始处的指针free。由于存活数据在Tospcae中式缩并的,检查空间是否足够仅仅只是一个指针比较而已。节点复制式收集器能够自然地处理可变大小的对象,因此我们把对象的大小作为参数n传递给New过程。和Mark-Sweep技术一样,节点复制技术不会给像更新这样的用户程序操作带来外的负担。

//节点复制式收集器中的分配init() =    Tospace = Heap_bottom    space_size = Heap_size / 2    top_of_space = Tospace + space_size    Fromspace = top_of_space + 1    free = TopaceNew(n) =    if free + n > top_of_space        filp()    if free + n > top_of_space        abort "Memory exhausted"    newcell = free    free = free + n    return newcell

1.0 简单的节点复制算法


首先,过程flip交换Tospace和Fromspace的角色,它重置了变量Tospace,Fromspace和top_of_space。接着,收集器把每个从根出发可以代打的单元都从Fromspace复制到Tospace,为了能够说明的比较清楚,这里给出了简单的递归算法【Fenichel and Yochelson ,1969】;在以后的博文中会进一步讨论节点复制技术的更轻巧的迭代算法。Copy(P)复制P指向的单元中的各个域,复制数据结构时必须小心地保持共享结构的拓补,否则将会导致共享对象出现多个副本。在最好的情况下,者将增大程序堆维空间的占有率,如果没有这么幸运,这种错误可能破环用户程序的语义(例如,如果用户程序更新了某个单元的一个副本,但却读取了另一个副本的数据)。此外,若没能保持共享,复制环形数据结构将会需要很大的空间!

//节点复制式垃圾收集器中的切换过程flip() =    Promspace, Tospace = Tospace, Fromspace    top_of_space = Tospace + space_size    free = Tospace    for R in Roots        R = copy(R)

在复制节点时,节点复制收集器为Fromspace中的对象保留一个迁移地址,以此来保持共享。迁移地址实际上时一个Tospace中的副本地址。每当Fromspace中的某个单元被访问时,Copy过程会检查它是否已经被复制了。若是,那么就返回它的迁移地址,否则就在Tospace中保留空间以备复制。在这个递归的复制算法中,将迁移地址置为保留的空间的地址的这一动作,在复制构成这个对象的各个域之前完成:这保证了算法能够终止,并且共享得以保持。

迁移地址可能存放在单元内专门为它保留的域中。但更普遍的做法是将它写入单元的第一个字:前提是已经预先保存了这个字的内存。在下面给出的算法中,假设单元P中保存迁移地址的域是P[0],并且我们等价地使用forwarding_address(P)和P[0]。

//支持可变大小单元的Penichel Yochelson节点复制垃圾收集//P指向一个字,而不是一个单元copy(P) =    if atomic(P) or P == null        return P    if not forwarded(P)        n = size(P)        P_ = free //在Tospace中保留空间        free = free + n        temp = P[0]  //迁移地址将保存在第0域中        forwarding_address(P)= P_        P_[0] = copy(temp)        for i = 1 to n-1  //将P中的各个域复制到P_中            P_[i] = copy(P[i])    return forwarding_address(P)

1.1 节点复制算法的优势与缺点


同引用计数和Mark-Sweep相比,节点复制技术具有一定优势,这使得它得到了广泛得采用。采用节点复制技术可以极大地降低内存分配得开销:检查空间是够韩进只需要佐简单的指针比较:获取新内存可以简单地通过递增自由空间指针来实现;由于存活数据缩并地排列在Tospace的底部,内存碎片问题不复存在。相对地,在非缩并算法中分配的代价要高得多,特别是在要求单元大小可变得情况下。很难想象如果能够让内存分配操作得代价比节点复制更低了。

节点复制式垃圾收集最直接得代价就是使用了两个半区:这一依赖,它所需要得地址空间是非复制式收集起的两倍。常常有人争辩说这并不是个问题,因为采用虚拟存储器的系统会将补货送的半区paged-out到辅助存储器上,但是这种观点忽略了paged-out的开销。假设堆的大小固定,我们比较一下节点复制和Mark-Sweep这两种算法在两个垃圾收集周期内的行为。在这样一个时间段内,不论用户程序的内存占用率如何,检点复制式收集器将会触及堆中的每一页。除非两个半区能同时存放在物理内存中,否则由于节点复制式收集器所使用的页的数量是Mark-Sweep式收集器的两倍,它将会遇到更多的缺页错误。

另一方面,这一问题必须同缩并带来的好处放在一起加以权衡。采用简单的Mark-Sweep技术,堆中的数据很可能变得更加破碎,从而导致程序的工作集所包含的页面数量增加。如果工作集达到无法容纳在主存储器中,缺页率也会升高。引用局部性的丧失还会影响cache性能,但是与缺页比起来,这对总体性能的影响要小得多了。

2 0