Garbage Collection | Mark-Sweep算法

来源:互联网 发布:大数据融合技术 kettle 编辑:程序博客网 时间:2024/06/01 08:42

Mark-Sweep算法


这是第一种用于自动内存管理的算法,标记-清扫(mark-sweep)算法【McCartby ,1960】.在这一方案下,内存单元并不会在变成垃圾的同时立刻回收,而是保持不可到达和未被发现的状态,直到所有可用的内存都耗尽。如果此时再次出现对新单元的请求,系统会暂时挂起“有用”的程序,并调用垃圾收集例程,将堆中所有当前并未使用的单元清扫回自由单元池中。标记-清扫算法依靠对所有存活单元进行一次全局遍历来确定哪些单元可以回收。这个遍历从根出发,标明所有可以到达的单元。根据定义,这些就是存活单元。除此之外,所有其他节点都是垃圾,可以送回自由单元池。如果垃圾收集器成功地回收了足够的内存,用户程序的请求就能得到满足,并且可以继续进行工作。


1.0 简单的Mark-Sweep算法

现在,让我们更仔细地考察这个算法。New过程从池中请求一个新单元并返回一个指向它的指针。我们并不指明allocate究竟做些什么操作,但抽象地使用free_pool来描述自由单元的集合。一种可能的实现是把自由单元链接成一个自由链表,就像在上一篇博文中述说的引用计数算法中所做的一样,但是除此之外,我们还有其他效率更高的选择。这点会在以后的博文中有所讨论。

//Mark-Sweep算法的分配New() =    if free_pool is empty        mark_sweep()    newcell = allocate()    return new cell

采用mark-sweep算法,更新指针的操作不需要做额外的工作,直接写入即可。这与引用计数算法形成了显著的对比:后者需要几条额外的指令来操作引用计数值。如果者导致了cache错误等的失误,如果目标单元所在的内存页面当前恰好处于paged-out状态,引用计数的代价会更大。

mark-sweep式垃圾收集的执行分为两个阶段。第一个阶段,也就是被称为marking的阶段,标明所有存活单元。第二个阶段,sweep阶段,将垃圾单元归还给自由单元池。如果sweep阶段没能恢复足够多的自由单元,那么系统必须扩展堆,或者终止程序的运行。

//Mark-Sweep算法垃圾收集器mark_sweep() =    for R in Roots        mark(R)    sweep()    if free_pool is empty       abort "Memory exhausted"

每个单元需要保留一个二进制位让垃圾收集器使用。这个标记位用来记录是否能否从根出发到达这个单元。当mark过程遍历所有从根出发可以到达的单元时,每个被访问的单元的标记位都会被值位。这里给出简单的递归算法。

//简单的递归标记算法mark(N) =     if mark_bit(N) == unmarked         mark_bit(N) = marked         for M in Children(N)             mark(*M)

标记过程不会沿着已经被标记的单元追踪下去,这确保了标记阶段能够终止。当标记阶段结束时,所有从根出发可到达的单元的标记位都会被置位。由于从根出发无法到达任何未被标记的单元,因此它们一定是垃圾。

我们可以安全地将这些未被标记的单元归还给自由单元池。这是sweep阶段的工作。收集器从堆的底部出发,线性地清扫整个堆,将未被标记的单元放回自由单元池中,同时清除存活单元的标记位,为下一个垃圾收集周期做好准备。同样地,我们并不定义free,只是声明这个过程将它的参数所代表的单元放回自由单元池,使其可以再生使用。

//对堆的eager sweepsweep() =    N = Heap_buttom    while N <Heap_top        if mark_bit(N) == unmarked            free(N)        else mark_bit(N) = unmarked        N = N + size(N)

1.1 Mark-Sweep算法的优势与缺点


与引用计数算法相比,mark-sweep算法拥有两个优点。这使得它被一些系统所采用(如函数式语言 Miranda【Turner ,1985】和Boehm 保守式垃圾收集器【Boehm and Weiser ,1988】).第一,它可以非常自然地处理环形结构,不需要采取特别的预防措施;第二,操作指针没有额外的开销。然而在另一方面,mark-sweep是一种“停止-启动”算法:在垃圾收集器运行时用户程序的运行必须暂停,而且由mark-sweep算法造成的中断可能是巨大的。在20世纪80年代早期,Fateman【Foderaro and Fateman ,1981】发现由于内存容量比处理速度增长更快,某些大型Lisp程序花在mark与sweep上的运行时间占总运行时间的25%到40%,而用户平均每79s就需要等待4.5s,这是不可忍受的。不可中断的,采用全局遍历的mark-sweep算法对于实时系统,高交互行的系统和分布式系统而言并不实用。关键性的安全系统,实时系统,甚至是视频游戏,都不可能接受在垃圾收集时由这么长的停顿。对此,产生一个解决方案是在关键的时间段内禁止垃圾收集,这一点将在以后的博文中有所提及。

然而,如果响应时间不是个重要的因素,那么mark-sweep算法能够提供比像引用计数这样的渐进式算法更好的性能。不过,它的代价还是较高。在mark阶段,收集器必须标记每个存活单元,然后在sweep阶段检查所有单元。因此,这一算法的渐进复杂度正比于堆的大小,而非仅仅正比于存活单元的数量。

此外,上述给出的简单mark-sweep算法还会倾向于使内存空间更破碎,让单元散步在整个堆中。在实际存储器系统中,尽管内存破碎会使采用cache带来的益处不复存在,但它对性能的影响可能还不是很大。而在虚拟存储系统中,这种破碎会造成同一数据结构的相关单元之间失去空间上的局部性,从而导致系统出现thrashing现象:程序极为频繁地在辅助存储器和主存储器之间交换页面。并且,不论实存储器还是虚拟存储器系统,这种破碎会使得分配内存更加困难,这是因为必须在堆中搜寻何时的“空隙”以容纳新的对象。

追踪式的垃圾收集机制要像有效运行,堆中也需要一些净空间。假设分配的速率为常数,切无需考虑内存破碎的问题,那么两次收集之间的间隔就取决于每次都记中所回收的自由空间的数量。因此,随着程序占用越来越多的堆空间,或者说随着内存占有率的增长,垃圾收集会变得越来越频繁,用户程序对处理器的占用率会下降。换句话说,垃圾收集也会由thrashing现象。另一方面,引用计数系统的性能则不会因为堆空间占用率的提高而下降(尽管内存破碎可能会影响分配行为)。


3 0
原创粉丝点击