我们不能失去信仰

我们在这个世界上不停地奔跑...

0%

Python垃圾回收机制

小整数对象池

整数在程序中的使用非常广泛,Python 为了优化速度,使用了小整数对象池, 避免为整数频繁申请和销毁内存空间。

Python 对小整数的定义是 [-5, 257) 这些整数对象是提前建立好的,不会被垃圾回收。 Python 程序中,所有位于这个范围内的整数使用的都是同一个对象。

同理,单个字母也是这样的。

大整数对象池

每个大整数,均创建一个新的对象。

1
2
3
4
5
6
7
8
9
10
>>> a = 1233
>>> id(a)
140292687716768
>>> b = 1233
>>> id(b)
140292687716744
>>> c = 1234
>>> id(c)
140292687716696
>>>

对大整数是每次都创建一个新的对象。 对字符串并不是。

intern 机制

1
2
3
4
5
6
7
>>> a = "HelloWorld"
>>> id(a)
4361460880
>>> b = "HelloWorld"
>>> id(b)
4361460880
>>>

Python 中有这样一个机制称为intern 机制,让他只占用一个 HelloWorld 的内存空间。靠引用计数去维护何时释放。

总结

  • 小整数[-5,257)共用对象,常驻内存(可以更改,但是要重新编译 Python)
  • 单个字符共用对象,常驻内存。
  • 单个单词,不可修改,默认开启 intern 机制,共用对象,当引用计数为 0 时就销毁。
  • 数值类型和字符串类型在 Python 中都是不可变的,这意味着你无法修改这个对象的值,每次对变量的修改,实际上是创建一个新的对象。

Python 垃圾回收

Python 里也像 Java 一样采用了垃圾回收机制。不一样的是, Python 是以引用计数机制为主,标记-清除和分代收集两种机制为辅的策略。

  • 引用计数机制

Python 里万物皆对象,它们的核心就是一个结构体:PyObject

1
2
3
4
typedef struct_object {
int ob_refcnt;
struct_typeobject *ob_type;
} PyObject;

PyObject 是每个对象必有的内容,其中 ob_refcnt 就是作为引用计数的变量。当一个对象有新的引用时,它的 ob_refcnt 就会增加,当引用它的对象被删除,它的 ob_refcnt 的值就会减少。

1
2
3
#define Py_INCREF(op)   ((op)->ob_refcnt++) //增加计数
#define Py_DECREF(op) //减少计数
if (--(op)->ob_refcnt != 0) ; else __Py_Dealloc((PyObject *)(op))

当引用计数为 0 时,该对象生命就结束了。

引用计数机制的优点

  • 简单
  • 实时性:一旦没有引用,内存就直接释放了。不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时。

引用计数机制的缺点

  • 维护引用计数消耗资源

  • 循环引用

    1
    2
    3
    4
    list1 = []
    list2 = []
    list1.append(list2)
    list2.append(list1)

list1list2 相互引用,如果不存在其他对象对它们的引用,list1list2 的引用计数也仍然为1,所占用的内存永远无法被回收。 对于如今的强大硬件,缺点1尚可接受,但是循环引用导致内存泄露,注定 Python 还需要引入新的垃圾回收机制。(标记清除和分代收集)

画说 Ruby 与 Python 垃圾回收

英文原文: visualizing garbage collection in ruby and python

以下内容源自上面这篇文章

应用程序那颗跃动的心

GC 系统所承担的工作远比”垃圾回收”多得多。实际上,它们负责三个重要任务。它们

  • 为新生成的对象分配内存
  • 识别那些垃圾对象
  • 回收垃圾对象占用的内存

如果将应用程序比作人的身体,你所写的那些优雅的代码、业务逻辑、算法应该就是大脑。以此类推,垃圾回收机制应该是哪个个身体器官呢?

我认为垃圾回收就是应用程序那颗跃动的心。像心脏为身体其他器官提供血液和营养物那样,垃圾回收器为你的应该程序提供内存和对象。如果心脏停跳,过不了几秒钟人就完了。如果垃圾回收器停止工作或运行迟缓,像动脉阻塞,你的应用程序效率也会下降,直至最终死掉。

一个简单的例子

运用实例一有助于理论的理解。下面是一个简单类,分别用 Python 和 Ruby 书写

img

顺便提一句,两种语言的代码竟能如此相像:Ruby 和 Python 在表达同一事物上真的只是略有不同。但是在这两种语言的内部实现上是否也如此相似呢?

Ruby 的对象分配

当我们执行上面的 Node.new(1) 时,Ruby 到底做了什么?Ruby 是如何为我们创建新的对象的呢? 出乎意料的是它做的非常少。实际上,早在代码开始执行前,Ruby 就提前创建了成百上千个对象,并把它们串在链表上,名曰:可用列表。下图所示为可用列表的概念图:

img

想象一下每个白色方格上都标着一个”未使用预创建对象”。当我们调用 Node.new ,Ruby 只需取一个预创建对象给我们使用即可。

img

上图中左侧灰格表示我们代码中使用的当前对象,同时其他白格是未使用对象。(请注意:无疑我的示意图是对实际的简化。实际上,Ruby会用另一个对象来装载字符串”ABC”,另一个对象装载Node类定义,还有一个对象装载了代码中分析出的抽象语法树,等等)

如果我们再次调用 Node.new,Ruby 将传递给我们另一个对象:

img

这个简单的用链表来预分配对象的算法已经发明了超过 50 年,而发明人正是赫赫有名的计算机科学家 John McCarthy,一开始是用 Lisp 实现的。Lisp 不仅是最早的函数式编程语言,在计算机科学领域也有许多创举。其一就是利用垃圾回收机制自动化进行程序内存管理的概念。

标准版的 Ruby,也就是众所周知的 Matz's Ruby Interpreter(MRI),所使用的 GC 算法与 McCarthy 在1960年的实现方式很类似。无论好坏,Ruby 的垃圾回收机制已经 53 岁高龄了。像 Lisp 一样,Ruby 预先创建一些对象,然后在你分配新对象或者变量的时候供你使用。

Python 的对象分配

我们已经了解了 Ruby 预先创建对象并将它们存放在可用列表中。那 Python 又怎么样呢?

尽管由于许多原因 Python 也使用可用列表(用来回收一些特定对象比如 list),但在为新对象和变量分配内存的方面Python 和 Ruby 是不同的。

例如我们用 Pyhon 来创建一个 Node 对象:

img

与 Ruby 不同,当创建对象时 Python 立即向操作系统请求内存。(Python 实际上实现了一套自己的内存分配系统,在操作系统堆之上提供了一个抽象层。但是我今天不展开说了)

当我们创建第二个对象的时候,再次像 OS 请求内存:

img

看起来够简单吧,在我们创建对象的时候,Python 会花些时间为我们找到并分配内存。

Ruby 开发者住在凌乱的房间里

img

Ruby 把无用的对象留在内存里,直到下一次 GC 执行

回过来看 Ruby,随着我们创建越来越多的对象,Ruby 会持续寻找可用列表里预创建的对象给我们。因此,可用列表会逐渐变短。

img

…然后更短。

img

请注意我一直在为变量 n1 赋新值,Ruby 把旧值留在原处。”ABC”,”JKL” 和 “MNO” 三个 Node 实例还滞留在内存中。Ruby 不会立即清除代码中不再使用的旧对象!Ruby 开发者们就像是住在一间凌乱的房间,地板上摞着衣服,要么洗碗池里都是脏盘子。作为一个 Ruby 程序员,无用的垃圾对象会一直环绕着你。

Python 开发者住在卫生之家庭

img

用完的垃圾对象会立即被 Python 打扫干净

Python 与 Ruby 的垃圾回收机制颇为不同。让我们回到前面提到的三个 Python Node 对象:

img

在内部,创建一个对象时,Python 总是在对象的 C 结构体里保存一个整数,称为 引用数。期初,Python 将这个值设置为1。

img

值为 1 说明分别有个一个指针指向或是引用这三个对象。假如我们现在创建一个新的 Node 实例 “JKL”

img

与之前一样,Python 设置 “JKL” 的引用数为1。然而,请注意由于我们改变了 n1 指向了 “JKL”,不再指向 “ABC”, Python 就把 “ABC” 的引用数置为 0 了。 此刻,Python 垃圾回收器立刻挺身而出!每当对象的引用数减为 0,Python 立即将其释放,把内存还给操作系统:

img

上面 Python 回收了 “ABC” Node 实例使用的内存。记住,Ruby 把旧对象置于原地不管,也不释放它们的内存。

Python 的这种垃圾回收算法被称为引用计数。是 George-Collins 在 1960 年发明的,恰巧与 John McCarthy 发明的可用列表算法在同一年出现。就像 Mike-Bernstein 在6月份哥谭市 Ruby 大会杰出的垃圾回收机制演讲中说的: “1960年是垃圾收集器的黄金年代…”

Python 开发者工作在卫生之家,你可以想象,有个患有轻度OCD(一种强迫症)的室友一刻不停地跟在你身后打扫,你一放下脏碟子或杯子,有个家伙已经准备好把它放进洗碗机了!

现在来看第二例子。加入我们让 n2 引用 n1:

img

上图中左边的 DEF 的引用数已经被 Python 减少了,垃圾回收器会立即回收 DEF 实例。同时 JKL 的引用数已经变为了 2 ,因为 n1 和 n2 都指向它。

2.7 标记-清除

最终那间凌乱的房间充斥着垃圾,再不能岁月静好了。在 Ruby 程序运行了一阵子以后,可用列表最终被用光光了:

img

此刻所有 Ruby 预创建对象都被程序用过了(它们都变灰了),可用列表里空空如也(没有白格子了)。

此刻 Ruby 祭出另一 McCarthy 发明的算法,名曰:标记-清除。首先 Ruby 把程序停下来,Ruby 用”地球停转垃圾回收大法”。之后 Ruby 轮询所有指针,变量和代码产生别的引用对象和其他值。同时 Ruby 通过自身的虚拟机遍历内部指针。标记出这些指针引用的每个对象。我在图中使用 M 表示。

img

上图中那三个被标 M 的对象是程序还在使用的。在内部,Ruby 实际上使用一串位值,被称为:可用位图(译注:还记得《编程珠玑》里的为突发排序吗,这对离散度不高的有限整数集合具有很强的压缩效果,用以节约机器的资源。),来跟踪对象是否被标记了。

img

如果说被标记的对象是存活的,剩下的未被标记的对象只能是垃圾,这意味着我们的代码不再会使用它了。我会在下图中用白格子表示垃圾对象:

img

接下来 Ruby 清除这些无用的垃圾对象,把它们送回到可用列表中:

img

在内部这一切发生得迅雷不及掩耳,因为 Ruby 实际上不会把对象从这拷贝到那。而是通过调整内部指针,将其指向一个新链表的方式,来将垃圾对象归位到可用列表中的。

现在等到下回再创建对象的时候 Ruby 又可以把这些垃圾对象分给我们使用了。在 Ruby 里,对象们六道轮回,转世投胎,享受多次人生。

标记-删除 vs. 引用计数

乍一看,Python 的 GC 算法貌似远胜于 Ruby 的:宁舍洁宇而居秽室乎?为什么 Ruby 宁愿定期强制程序停止运行,也不使用 Python 的算法呢?

然而,引用计数并不像第一眼看上去那样简单。有许多原因使得许多语言不像 Python 这样使用引用计数 GC 算法。

首先,它不好实现。Python 不得不在每个对象内部留一些空间来处理引用数。这样付出了一小点儿空间上的代价。但更糟糕的是,每个简单的操作(像修改变量或引用)都会变成一个更复杂的操作,因为 Python 需要增加一个计数,减少另一个,还可能释放对象。

第二点,它相对较慢。虽然 Python 随着程序执行 GC 很稳健(一把脏碟子放在洗碗盆里就开始洗啦),但这并不一定更快。Python 不停地更新着众多引用数值。特别是当你不再使用一个大数据结构的时候,比如一个包含很多元素的列表,Python 可能必须一次性释放大量对象。减少引用数就成了一项复杂的递归过程了。

最后,它不是总奏效的。引用计数不能处理环形数据结构–也就是含有循环引用的数据结构。

Python 中的循环数据结构以及引用计数

循环引用

通过上篇,我们知道在 Python 中,每个对象都保存了一个称为引用计数的整数值,来追踪到底有多少引用指向了这个对象。无论何时,如果我们程序中的一个变量或其他对象引用了目标对象,Python 将会增加这个计数值,而当程序停止使用这个对象,则 Python 会减少这个计数值。一旦计数值被减到零,Python 将会释放这个对象以及回收相关内存空间。

从六十年代开始,计算机科学界就面临了一个严重的理论问题,那就是针对引用计数这种算法来说,如果一个数据结构引用了它自身,即如果这个数据结构是一个循环数据结构,那么某些引用计数值是肯定无法变成零的。为了更好地理解这个问题,让我们举个例子。下面的代码展示了一些上周我们所用到的节点类:

img

我们有一个”构造器”(在Python中叫做 init ),在一个实例变量中存储一个单独的属性。在类定义之后我们创建两个节点,ABC 以及 DEF,在图中为左边的矩形框。两个节点的引用计数都被初始化为 1,因为各有两个引用指向各个节点( n1 和 n2 )。

现在,让我们在节点中定义两个附加的属性,next 以及 prev:

img

跟 Ruby 不同的是,Python 中你可以在代码运行的时候动态定义实例变量或对象属性。这看起来似乎有点像 Ruby 缺失了某些有趣的魔法。(声明下我不是一个 Python 程序员,所以可能会存在一些命名方面的错误)。我们设置 n1.next 指向 n2,同时设置 n2.prev 指回 n1。现在,我们的两个节点使用循环引用的方式构成了一个双向链表。同时请注意到 ABC 以及 DEF 的引用计数值已经增加到了2。这里有两个指针指向了每个节点:首先是 n1 以及 n2,其次就是 next 以及 prev。

现在,假定我们的程序不再使用这两个节点了,我们将 n1 和 n2 都设置为null(Python中是None)。

img

好了,Python 会像往常一样将每个节点的引用计数减少到 1 。

在 Python 中的零代(Generation Zero)

请注意在以上刚刚说到的例子中,我们以一个不是很常见的情况结尾:我们有一个“孤岛”或是一组未使用的、互相指向的对象,但是谁都没有外部引用。换句话说,我们的程序不再使用这些节点对象了,所以我们希望 Python 的垃圾回收机制能够足够智能去释放这些对象并回收它们占用的内存空间。但是这不可能,因为所有的引用计数都是1而不是0。Python 的引用计数算法不能够处理互相指向自己的对象。

这就是为什么 Python 要引入Generational GC算法的原因!正如 Ruby 使用一个链表(free list)来持续追踪未使用的、自由的对象一样,Python 使用一种不同的链表来持续追踪活跃的对象。而不将其称之为“活跃列表”,Python的内部 C 代码将其称为零代(Generation Zero)。每次当你创建一个对象或其他什么值的时候,Python 会将其加入零代链表:

img

从上边可以看到当我们创建 “ABC” 节点的时候,Python 将其加入零代链表。请注意到这并不是一个真正的列表,并不能直接在你的代码中访问,事实上这个链表是一个完全内部的 Python 运行时。 相似的,当我们创建 “DEF” 节点的时候,Python 将其加入同样的链表:

img

现在零代包含了两个节点对象。(他还将包含 Python 创建的每个其他值,与一些 Python 自己使用的内部值。)

检测循环引用

随后,Python 会循环遍历零代列表上的每个对象,检查列表中每个互相引用的对象,根据规则减掉其引用计数。在这个过程中,Python 会一个接一个的统计内部引用的数量以防过早地释放对象。

为了便于理解,来看一个例子:

img

从上面可以看到 ABC 和 DEF 节点包含的引用数为 1 。有三个其他的对象同时存在于零代链表中,蓝色的箭头指示了有一些对象正在被零代链表之外的其他对象所引用。(接下来我们会看到,Python 中同时存在另外两个分别被称为一代和二代的链表)。这些对象有着更高的引用计数因为它们正在被其他指针所指向着。

接下来你会看到 Python 的 GC 是如何处理零代链表的。

img

通过识别内部引用,Python 能够减少许多零代链表对象的引用计数。在上图的第一行中你能够看见 “ABC”和 “DEF” 的引用计数已经变为零了,这意味着收集器可以释放它们并回收内存空间了。剩下的活跃的对象则被移动到一个新的链表:一代链表。

从某种意义上说,Python 的 GC 算法类似于 Ruby 所用的标记回收算法。周期性地从一个对象到另一个对象追踪引用以确定对象是否还是活跃的,正在被程序所使用的,这正类似于 Ruby 的标记过程。

Python 中的 GC 阈值

Python 什么时候会进行这个标记过程?随着你的程序运行,Python 解释器保持对新创建的对象,以及因为引用计数为零而被释放掉的对象的追踪。从理论上说,这两个值应该保持一致,因为程序新建的每个对象都应该最终被释放掉。

当然,事实并非如此。因为循环引用的原因,并且因为你的程序使用了一些比其他对象存在时间更长的对象,从而被分配对象的计数值与被释放对象的计数值之间的差异在逐渐增长。一旦这个差异累计超过某个阈值,则 Python的收集机制就启动了,并且触发上边所说到的零代算法,释放“浮动的垃圾”,并且将剩下的对象移动到一代列表。

随着时间的推移,程序所使用的对象逐渐从零代列表移动到一代列表。而 Python 对于一代列表中对象的处理遵循同样的方法,一旦被分配计数值与被释放计数值累计到达一定阈值,Python 会将剩下的活跃对象移动到二代列表。

通过这种方法,你的代码所长期使用的对象,那些你的代码持续访问的活跃对象,会从零代链表转移到一代再转移到二代。通过不同的阈值设置,Python 可以在不同的时间间隔处理这些对象。Python 处理零代最为频繁,其次是一代然后才是二代。

弱代假说

年轻的对象通常死得也快,而老对象则很有可能存活更长的时间。

假定现在我用 Python 或是 Ruby 创建一个新对象:

img

根据假说,我的代码很可能仅仅会使用 “ABC” 很短的时间。这个对象也许仅仅只是一个方法中的中间结果,并且随着方法的返回这个对象就将变成垃圾了。大部分的新对象都是如此般地很快变成垃圾。然而,偶尔程序会创建一些很重要的,存活时间比较长的对象-例如 Web 应用中的 Session 变量或是配置项。

通过频繁的处理零代链表中的新对象,Python 的垃圾收集器将把时间花在更有意义的地方:它处理那些很快就可能变成垃圾的新对象。同时只在很少的时候,当满足阈值的条件,收集器才回去处理那些老变量。