Garbage Collection | 引用计数的改善考察(一)

1 非递归的释放


之前的博文中,所介绍的简单的引用计数算法中,每当指向某个对象的指针被改写的时候,Update过程就会减小那个对象的引用计数数值。如果计数值变为0,那么在将该对象所占据的内存归还给自由链表之前,必须递归地删除这个对象所包含的指针。因此,简单的递归释放在散布处理开销时并不均匀:删除指向某个对象的最后一个指针的代价不是常数,甚至不是正比于对象大小,而是依赖于以改对象为根的子图的大小。


1.1 延迟释放算法


Weizenbaum提出了一个方法,把自由链表当作一个栈来使用,借此“平滑”释放动作【Weizenbaum ,1963】.当指向节点N的最后一个指针被删除时,系统简单地把N压入自由栈。此时并不进行递归释放。取而代之的,当要从自由栈顶取出N重新分配时,New例程会对N中的所有指针执行delete操作,如果其中某个指针指向的节点的引用计数值变为0,那么再把这个节点压入自由栈。有一点很重要:再单元入栈时,单元中所有指针域的内容都不能破坏。可以确定不再需要的只有引用计数域(因为如果单元时自由的,这个域一定为0),因此我们可以用它来把自由栈链接起来。通过推迟对垃圾的检测来实现延迟释放,我们可以沿用之前所给的算法中free跟Update的过程,只需要将用来链接自由链表的域从原先未指明的next域改成RC域即可。但是,New和delete过程则必须修改。此外,我们使用incremenrRC和decrementRC来抽象调整引用计数域的底层细节。这么做的原因将在本文第3段会给出。

//Weizenbaum 用于引用计数的延迟释放算法
New() =
    if free_list == null
        abort "Memory exhausted"
    newcell = allocate()
    for N in Children(newcell)
        delete(*N)
    RC(newcell) = 1
    return newcell

delete(N) =
    if RC(N) == 1
       RC(N) = free_list
       free_list = N
    else decrementRC(N)

1.2 延迟释放的优点和代价


延迟释放和原有的急切释放在效率上时相同的(执行的指令完全相同,只是从过程delete移到过程allocate中而已),但是算法不再那么容易受到被释放单元的后代所造成的延迟影响了。遗憾的是,这并不能完全解决处理开销不均匀的问题。如例,如果释放了一个数组,那么当它来到自由链表的顶部时,还是必须删除它的所有指针(虽然深度只有一层);删除指针,调整自由栈所带来的延迟可大可小,视数组大小而定。Weizenbaum算法的延迟性也失去了标准引用计数的立即性所带来的某些好处。垃圾数据结构各部分所占据的内存会一直无法访问,直到整个数据结构在自由栈的栈顶被New过程移除为止。假设 某个类型的对象分为对象头和对象体两部分,每个对象有一个较小的对象头表示,而这个对象头指向一个巨大的对象体。如果删除一个这样的对象,那么只有它的对象头会被压入到自由栈。如果另外再有几个对象也被删除并被压入自由栈的话,第一个被删除对象的对象体所占据的大量内存将不再视立刻可用的了。


2 延迟引用计数


在常规的赢家你上,维护引用计数值的开销很高。这使得引用计数作为一个内存管理机制不如基于追踪的方式那么具有吸引力。改写一个指针通常需要10几条指令,以调整指针所指向的旧目标和新目标的引用计数值。当指针被压入,或弹出系统栈的时候,也必须天正引用计数值。甚至视向遍历一个列表这样无破坏性的操作也必须在经过每个元素时先增大然后减少它的引用计数值。在现代的,拥有数据cache的突袭结构中,读取计数值的指令可能导致原本根本不会碰到的数据进入cache。这些数据会被污染,从而必须写回堆内存,尽管他们的值和把它们放入cache中时完全一样。更坏的结果是,操纵引用计数值可能导致保存对象的内存页面被换入。

减小这个开销的唯一途径,是抓住每个安全的机会避开堆计数值的调整。在手工编写的引用计数系统中,一个经常得到使用的技术是避免在进入和退出子例程时增大和减小参数的计数值。这只有在能够确定子例程的执行不会导致参数的计数值降为0时才是安全的。手工的引用计数优化很可能是以延长调试时间来换取缩短CPU时间。把优化其放入编译器中时隔更可靠的方案;在SISAL的并行实现中,这一方案已经被证明能够有效地消除对计数值的操作。非正统的系统类型也可能被用来识别单绪对象,使引用计数成为不必要的。Baker曾经鼓吹过使用基于线性逻辑的类型系统【Girard ,1987】,他视之为一个有效的技术,然而其他人却发现在实践中这一技术让人失望【Baker ,1994;Walkling ,1990】.函数式程序设计语言Clean采用了一个类似的unique types系统【Brusetal ,1987】.尽管这些系统要求程序员来识别单绪的对象,然而它们的type assertion的正确性能够有编译器来检查。


2.1 Deutsch-Bobrow算法


与那些试图通过编译时分析来消除对计数值的操作的人不同,Deutsch和Bobrow设计了一套系统化的运行时方法来推迟对引用计数值的调整【Deutsch and Bobrow ,1976】。在程序中,大部分的指针赋值都是将指针保存到某个局部变量中;有了现代的,优化的Lisp或ML编译器,其他的指针赋值的比率低于1%。延迟引用计数利用了这个发现,对局部变量和栈分配的编译器临时量进行特殊处理:当它们被改变时,不做任何引用计数的薄记工作。因此,将指针写入到局部的名字这一操作就能使用简单的赋值而非Update过程。现在引用计数值仅仅反应了来自堆中其他对象的引用数量:来自栈的引用并不计算在内。这意味着我们不再能够在对象的计数值降为0时同时回收它们了,因为它们仍然可能通过某个局部变量或者临时变量直接到达。取而代之的是,delete将计数值变为0的单元放入一个名未“zero count table”的表中(简称ZCT)。ZCT通常以一个哈希表或者位图来实现。

当指向ZCT中某个对象的指针被保存到另一个堆对象中时,系统会增大它的计数值并删除ZCT中对应的条目。收集器周期性地核对ZCT以回收垃圾。任何对象,如果ZCT中存在指向它的引用,而且在扫描栈时也无法找到它,那么这个对象一定是垃圾,可以送回自由链表了。核对工作分为3个阶段:首先标记所有可以从栈直接访问的对象,接着释放ZCT中所有未被标记的对象。最后将所有被标记对象的标记擦去。

//延迟引用计数:更新指针值
delete(N) =
    decrementRC(N)
    if RC(N) == 0
       add N to ZCT

Update(R ,S) //R和S是堆中对象
    incrementRC(S)
    delete(*R)
    remove S from ZCT
    *R = S

标记对象和清除对象白哦及的方法质疑是分别增大和减小它们的引用计数域。ZCT中的对象,如果它真的是垃圾,那么在增大了所有可以直接从栈到达的对象计数值之后,它的计数值一定还是0.在对这些对象所保存的全部指针执行delete操作之后,旧可以释放它们了。最后,在第一阶段过程中增大了的引用计数值,必须相应的减小。

//延迟引用计数:核对ZCT
reconcile() =
    for N in stack
        incrementRC(N) //标记栈中对象
    for  N in ZCT   //回收垃圾
        if RC(N) == 0
           for M in Children(N)
               delete(*M)
           free(N)
    for N in stack  //清除栈中对象标记
        decrementRC(N)

2.2 ZCT溢出


要是ZCT在溢出的时候进行核对,递归地释放对象可能在每次释放一个对象时向ZCT中添加更多的条目。针对这个难题,有几种不同的解决方案。如果释放一个对象将会导致ZCT溢出,那么可以中断它的回收,把这个对象留在ZCT中,直到下一次核对。或者,我们可以采用Weizenbaum的延迟释放技术,在释放对象时并不删除它所保存的各个指针,直到这个对象被重新分配。系统可以在分配动作即将导致溢出时核对ZCT。又或者,如果用位图实现ZCT,那么溢出旧不再成为一个问题。在我们讨论垃圾收集时,位图一般是指由二进制位构成的数组,每一位代表了堆中的一个字。系统通过设置和清除对应于某个对象的二进制位来代表这个对象进入或者离开ZCT。以一小部分堆空间为代价,我们可以完全省去对溢出的检查。

2.3 延迟引用计数的效率


延迟引用计数在减小指针写的开销方面非常有效。20世纪80年代中期在Xerox Dorado上实现Smalltalk的经验标明,一般来说它能够把操作指针的开销降低80%甚至更多,而只要付出相对较小的空间上的代价。Ungar对标准引用计数系统和延迟引用计数系统中指针更新,核对,递归释放操作进行了比较。还声称核对ZCT所带来的停顿(没500ms停顿30ms)也要比那些mark-sweep垃圾收集带来的停顿要短。

项目标准引用计数延迟引用计数
更新指针153
核对ZCT\5
递归释放55
总计2011

延迟引用计数也有一些缺点。出了ZCT带来空间开销之外,它的主要缺点就是变成垃圾的对象必须等到核对ZCT时才能回收,着缩小了引用计数技术在立刻回收内存方面的优势。

to be continued....

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
垃圾收集(Garbage Collection)是一种自动内存管理机制,即自动回收不再使用的内存空间。在Python中,垃圾收集器通过搜索对象,并查找引用计数为0的对象来找到要回收的内存空间。 以下是Python中垃圾收集器的一些概念和过程[^1]: - 引用计数:当一个对象被创建时,它的引用计数被设置为1,当有一个新的变量引用它时,它的引用计数会增加1,当这个引用变量被删除时,它的引用计数就会减1。当引用计数为0时,该对象不再被引用,就会被垃圾收集器回收。 - 标记-清除:Python的垃圾收集器通过标记-清除算法来回收内存。在这个过程中,垃圾收集器会首先标记所有被引用对象,然后清除所有未被标记的对象。在清除这些未标记对象时,并将它们的内存释放回内存池。 - 引用循环:引用循环是指两个或两个以上的对象相互引用,这种情况下它们的引用计数永远不会降为0,因此不会被垃圾收集器回收。Python使用“标记-清除”和“分代回收”机制来处理这种情况。 - 分代回收:分代回收是指Python中垃圾收集器将内存分成不同的代,每个代都有一个垃圾回收阈值。新创建的对象会被放置在第0代,当第0代的垃圾回收阈值达到时,升级到第1代。当第1代的垃圾回收阈值达到时,升级到第2代,以此类推。 以下是一个Python程序,展示了垃圾收集的过程: ```python import gc import sys a = [1, 2, 3] print(sys.getrefcount(a)) # 输出:2 b = [4, 5, 6] a.append(b) b.append(a) print(gc.get_count()) # 输出:(0, 0, 0) del a del b print(gc.get_count()) # 输出:(3, 0, 0) gc.collect() print(gc.get_count()) # 输出:(0, 0, 0) ``` 上述程序中,当a和b中的所有变量被删除后,使用gc.collect()方法可以触发垃圾收集器回收内存,输出gc.get_count()的结果为(0, 0, 0)。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值