原文
译文(摘取部分段落)
总结
引用环,包括lists, tuples, instances, classes, dictionaries和 functions被找到了。有del方法的instances处理得比较好。很容易可以给新的类型添加GC支持。
分代回收是可以工作的(目前是3个代)。pybench测量的开销是4%。
我们为什么需要它?
当前版本的Python使用引用计数来追踪所有被分配的内存。Python中的每一个对象中有一个引用计数,来指示有多少个对象指向了它。当这个引用计数变为0这个对象就会被释放。这个方法在大多数程序里工作得很好。然而,有一个重大的缺陷,就是引用环。最简单的引用环就是一个对象引用了自身:
>>> l = []
>>> l.append(l)
>>> del l
现在,创造的这个list的引用计数为1。然而,由于它在Python内部不能“够得着”,不可能被再使用了,它应该被认为是垃圾。在当前版本的Python,这个list永远不会被释放。
产生引用环通常是不好的编程,而且几乎总是可以被避免的。然而,有时候很难避免产生引用环,有些时候程序员甚至没有意识到这件事情的发生。对于长时间运行的程序如服务器,这是特别麻烦的事情。人们不想他们的服务器由于引用环没法释放不可达对象而用完了内存。对于大型的程序,很难发现引用环是怎么产生的。
传统的垃圾回收?
传统的垃圾回收(比如标记-清除或者停止-复制)的工作如下:
- 找系统的根对象。这是像全局变量的东西(像Python中的main模块)以及栈上的对象。
- 在这些对象中寻找,从他们出发找出所有可到达的对象。这些对象是“存活的”。
- 释放所有其他对象。
很不幸,这个方法不能在当前版本的Python中使用。因为扩展模块的工作方式,Python从来不能完全决定根集合。如果根集合不能被准确地确定,而我们可能释放了一些仍然在别的地方被引用的对象。就算扩展模块设计得不一样了,也没有办法找出哪些对象当前在C栈上被使用。另外,引用计数从内存引用本地化和析构语法这连个各方面提供给Python程序员期待的好处。如果我们可以找到一个方法,仍然使用引用计数,而且能释放引用环,那该多好。
这个方法怎么工作?
概念上讲,这个方法正好与传统的垃圾回收相反。不是去尝试找出所有可到达的对象,而是找出不可到达的对象。这是安全很多的,因为就算算法失败了我们最多也就是收集不到垃圾而已,(当然也浪费了时间和空间)。
因为我们仍然使用引用计数,垃圾回收只需要找出引用环。引用计数会处理其他类型的垃圾。首先我们发现引用环只能由容器对象产生。这是一些可以存放其他对象的引用的对象。在Python中,lists, dictionaries, instances, classes, 和 tuples都是容器对象的例子。Integers and strings不是容器对象。由这个发现我们意识到非容器对象可以忽略掉。这是有用的优化因为integers and strings这类对象很快很小。
我们的想法是追踪所有的容器对象。有几个方法可以实现这个要求,但是其中最好的是使用带有link field的双向链表。这允许对象可以快速的从集合中插入和移除,而且不需要更多的内存分配。当我们产生一个容器,它就被插入到集合中,删除这个容器的事后,它就从集合中被移除。
现在我们可以访问到所有的容器对象了,那么怎样找出引用环?首先,除了两个link指针以外,我们还在容器对象中加入一个另一个field。我们称这个field为gc_refs。以下是找出引用环的步骤:
- 对于每个容器对象,设置gc_refs等于该对象的引用计数。
- 对于每个容器对象,找出哪些容器对象它引用了,然后减小被引用容器的gc_refs
- 现在,所有的容器对象,如果有gc_refs大于1的,是被外面的容器对象引用的。我们不能释放这些对象,所以我们把他们移到另一个集合中。
- 任何被“被移除对象”引用的对象也不能被释放。我们将他们移除集合,也把他们所引用的对象从集合中移除。
- 在原来的集合中剩下的对象只是被集合内的对象引用。(也就是说,他们是不能从Python中被访问的,所以是垃圾)我们现在可以释放这些对象了。
finalizer的麻烦
我们伟大的计划有一个问题,那就是finalizer的使用。在Python,就是instances的__del__
方法的。和引用计数在一起,finalizer工作得很好。当一个对象的引用计数到达0,finalizer在对象释放之前被调用。这是很直接的,很容易可以被程序员理解。
现在有了垃圾回收,调用finalizer变成了一个困难的问题,尤其是面对引用环的时候。如果在一个引用环的两个对象都有finalizer,应该怎么做?哪个应该先被调用?在调用第一个finalizer之后对象不能被释放,因为第二个finalizer还有可能访问它。
由于这个问题没有好的解决方案,被对象引用的环没有被释放。这些对象被加入到一个全局的无法回收的垃圾列表。程序几乎总是可能被重写来保证这个情况不会出现。作为一个最后的招数,程序可以访问这个全局列表并找出一个对应用而言是有意义的方法,来释放引用环。
代价是什么?
人们常说,没有免费的午餐。然而,这种形式的垃圾回收相对还是比较“便宜”的。其中一个最大的开销是每个容器对象中的三个额外的字的内存。还有维护容器集合的开销。在当前本本的回收器,根据pybench,开销是4%。
回收器目前追踪三代对象。通过调整回收参数,回收的时间可以根据要求变小。在一些应用中,关闭自动回收,然后具体地调用回收例程是有道理的。然而,有了默认的回收参数,用来回收的时间看起来没那么重要了。显然,应用如果大量分配容器对象会引起更多的垃圾回收时间。
当前修订版增加了新的配置选项来使能回收机制。