Garbage Collection | 引用计数算法

时隔半个月了,距离上一次更新博文后,个人技术方向遇到瓶颈,失去研究方向后的感觉确实不好受。在一次偶然的问答中,发现自己对gc技术算法甚是模糊,总算找回了一点目标。

引用计数算法


这是一个比较直接的算法,其基本手段是为每一个单元计算指向它的引用(来自其他活动单元或者根)的数量【Collins,1960】.它的优点在于能够非常简单的判断单元是否正在使用。它还天生是一个渐进式的技术,能够将内存管理的开销分布到整个程序之中。基于引用计数的算法被许多语言和应用所采纳,例如,面向对象语言Smalltalk的早期版本,InterLisp与Adobe Photoshop。许多操作系统(如UNIX)也用这一方法来判断一个文件是否可以被删除。
从本质上来说,基于引用计数算法的垃圾收集器的工作策略。与那些基于追踪的垃圾收集器完全不同。每个单元都有一个额外的域,存放引用计数值。内存管理器必须为每一个单元维护引用计数值,使其等于指向该单元的指针的数量。首先,这一算法把所有的单元放在一个自由单元池里,这个池的实作常常是一个链表,连同一个指向链表表头的指针。当然,我们不一定非得为此专门增加一个next域(每个单元有一个指针域,简称next域,所有单元通过此链接成一条长链)。通常,它与保存引用计数值的域是同一个域—-自由单元并不需要明确的引用计数值。或者,我们也可以使用单元中某个保存用户数据的域。


1.0 简单引用计数算法


自由单元的引用计数值为0,当一个新单元从池中被分配的时候,它的引用计数值被设为1.每次有一个指针被设为指向这一单元时,该单元的计数值加1;而每次删除某个指向它的指针时,它的计数值减1。如果单元的计数值降为0,引用计数不变式告诉我们不再存在指向该单元的指针。更进一步,由于这个单元的位置被“丢失”,因此,也就不存在任何手段重新建立与这个单元的联系。程序已经不再需要这个单元,可以将其放回自由单元的列表了。

现在,让我们仔细地看一下这个算法。allocate是作为一个堆中分配空间的通用机制。在这里,它从自由链表中弹出第一个元素。New从自由链表中取出一个新单元,先把它的引用计数值置1,然后将其返回。若自由链表为空,程序就会终止(或者选择扩展堆容量);若自由链表非空,allocate取出自由链表的表头节点并返回给New。处于安全性的考虑,新单元的指针域也可能会被清除,虽然这么做可能并不必要—-如果单元在分配之后立即初始化它的各个域的话。在这种情况下,New可能也会从程序栈中取出一些参数并把它们填入单元的数据域。为了简化讨论,此时假定所有单元有着相同的固定大小。

//引用计数的分配
allocate()=
    newcell = free_list
    free_list = next(free_list)
    return newcell

New()=
    if free_list == null
       abort "Memory exhausted"
    newcell =allocate()
    RC(newcell) = 1
    return newcell

Update用它第二个参数S(假设是一个指针)来改写堆中由第一个参数R所指明的字的内容。由于有了新的引用,S的引用计数值加1.Update还移除了原有的从R到它的目标*R的指针,因此*R的引用计数值必须减1.通过先增加新目标的计数值,然后再减少老目标的计数值,我们处理了赋值前后的指针域指向同一目标的情况。假设R中的指针原来指向节点T。如果这个指针是指向T的最后一个引用,那么delete可以把T归还给自由链表。但在这么做之前,必须递归地堆所有从T出发的指针执行delete操作

//引用计数环境下更新指针域
free(N) =
    next(N) = free_list
    free_list = N

delete(T) =
    RC(T) = RC(T) - 1
    if RC(T) == 0
       for U in Children(T)
           delete(*U)
           free(T)
Update(R ,S) =
    RC(S) = RC(S) + 1
    delete(*R)
    *R = S

1.1 引用计数算法的优势与缺点


引用计数方法的长处在于内存管理的开销分布在整个计算过程之中,对存活单元和废弃单元的管理与用户程序的执行交织在一起。这一特点与那些非渐进式的,基于追踪的收集器(如标记清扫收集器)构成一个对比:这类垃圾收集器在运行时会挂起完成实际工作的用户程序。因此,若更“平滑”的响应时间比较重要(如高度交互性的系统或实时系统),那么引用计数可能是合适的选择。然而,上面给出的简单的引用计数算法在分布处理开销时存在“起伏”:删除通向某一子图的最后一个指针的代价,依赖于这个子图的大小。在以后的博文中,会更加细致的讨论并试图改进这一点。

与基于追踪的垃圾收集方案相比,引用计数方法的第二个优势,是它在空间上的引用局部性很可能不会劣于客户程序。当某个单元的引用计数值变为0时,系统无需访问位于堆中其他页面的单元就能回收它。这与那些基于追踪的算法形成了另一个对比:后者通常需要在回收废弃单元之前遍历所有存活单元。但是需要注意,在Update操作中,指针域被更新前后所指向的两个单元的引用计数域都必须被修改。如果这两个域中任意一个处于paged-out状态或者并未保存在数据cache中,就会发生缺页或cache错误。

第三,尽量对以往经验的研究很大程度上依赖于实作与语言,但是大范围的语言研究显示,只有少数单元会被共享,大多数单元是“短命”的。标准的引用计数方法允许这些单元以一种类似栈分配的方式在刚被丢弃时就立即回收重用。而在采用追踪算法的情况下,废弃单元会保持未分配状态知道整个堆耗尽—-此时垃圾收集器才会被调用。与简单的追踪式垃圾收集算法总是从堆中申请新鲜单元的方式相比,除非整个堆可以放进主内存或是cache中,否则立刻重用单元的方式在虚拟存储器系统中会产生更少的缺页错误,并可能表现出更好的cache行为。

能够立刻知道何时可以回收单元还会带来其他的好处。如果我们需要某个对象的一个修改过的副本,而且已经没有别的引用指向这一对象,那么我们可以不用非得分配一个新对象,然后一个字一个字地复制数据,最后释放老对象,我们完全可以直接借用指向现有对象的指针,“就地”修改对象的内容。引用计数还能简化像关闭文件这样的“清理”或者“终结”动作,因为它能够在对象死亡时立刻调用finalizer

另一方面,引用计数也有很多缺点,这些缺点使得许多是闲着认为它不是一个有效的内存管理方法。其中最严重的缺点就是为了维护引用计数不变式,这类实现不得不支付(采用现有常规硬件的情况下)高昂的处理开销。每次改写一个指针,它的旧目标单元与新目标单元的引用计数都必须进行调整。相反的,在简单的基于追踪的机制下,更新指针没有任何内存管理开销。

基于引用计数的内存管理机制总是与客户程序或它的编译器紧密地耦合在一起。每一次更新或者复制指针,都必须调整引用计数值。就像最简单的一种情况:这意味这当一个指针被传递给一个子过程时,必须正大引用计数,而返回时必须减少,只要有益处遗漏,旧可能带来灾难性的后果。与那些与用户程序耦合较松的内存管理系统相比,引用计数系统的这种脆弱性使得它们更加难以维护。

引用计数技术还必须在每个单元中使用额外的空间来存放引用技术值。在最坏的情况下,这个域必须大到足以存放下保存在堆节点和根中的指针的总数:它必须和一个指针一样大。在实际情况中,引用计数值并不会变得这么大,我们也可以使用一个较小的域(可能只有一个二进制位)并采用一个与其想配合的策略来处理溢出。这也会在以后的博文有所讨论。


1.2 无法回收环形数据结构


然而,简单的引用技术算法最主要的缺陷时它们无法回收环形的数据结构。对于许多系统,回收这类数据结构是个重要的需求。环形数据结构的使用频率比表面上看起来的要高。常见的环形结构的例子,包括双向链表和那些包含了从叶子回到根的“树”。此外,许多基于graph redution的延迟函数式语言的实现,也是采用环来处理递归。假设指针riht(R)被删除了,在函数调用delete(right(R))之内,S的引用计数值在减小之后依然不为0,因此控制被返回到用户程序。节点S,T和U并未被送入自由链表,相反,一个孤立于图的其他部分的“孤岛”产生了。接下来的计算不再需要这个孤岛,但是单元S,T和U却无法回收。堆内存中的一个区域泄露了。

其他垃圾收集计数可以毫无苦难你地处理环形数据结构,因此一些程序实作者建议将引用计数与追踪式的垃圾收集相结合。这种系统会首先一直使用引用计数机制,直到整个堆耗尽,并在那一刻唤醒追踪式的收集器。收集器在开始时将所有单元的引用ishu值置0.然后,标记程序在标记阶段每访问某个单元一次,该单元的计算值加1,以此来恢复各个存活单元的计数值。对于任意一个单元,标记程序通过每一个从其他存活单元指向它的指针访问它一次,因此,在标记阶段的最后,该单元的引用计数值就等于从其他存活单元指向它的指针的数量,而这恰好满足了引用计数不变式的要求。这一方法带来了两个收获。第一,环形结构可以通过追踪式收集器来回收。第二,能够使用较小的引用计数域(减少内存需求)。如果某个单元的计数值达到了这个较小的域所能存储的上限,那么Update将不再改动它的计数值,管理它们的责任将被移交给追踪式收集器。


  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值