1、循环引用

  • Python 中的每个对象都保存了一个称为引用计数的整数值,来追踪到底有多少引用指向了这个对象。
  • 如果程序中的一个变量或其它对象引用了目标对象,Python 将会增加这个计数值
  • 当程序停止使用这个对象,Python 会减少这个计数值
  • 一旦计数值被减到零,Python 将会释放这个对象以及回收相关内存空间
  • 从六十年代开始,计算机科学界就面临了一个严重的理论问题:针对引用计数算法,如果一个数据结构引用了它自身(即一个循环数据结构),则某些引用计数值是肯定无法变成零的。示例:

pythonfor循环语句获取序号_数据结构

  • "构造器"(Python 中即__init__)在一个实例变量中存储一个单独的属性。在类定义之后创建两个节点:ABCDEF,在图中为左边的矩形框。两个节点的引用计数都被初始化为 1,因为各有一个引用(n1n2)指向各个节点。
  • 现在在节点中定义两个附加的属性:nextprev

pythonfor循环语句获取序号_Python_02

  • Python 可以在代码运行时动态定义实例变量或对象属性,此处设置n1.next指向n2n2.prev指回n1。两个节点使用循环引用的方式构成了一个双向链表。
  • 注意:ABCDEF的引用计数值已经增加到了 2,因为有两个指针指向了每个节点:首先是n1n2,其次是nextprev
  • 假定程序不再使用这两个节点,因此将n1n2都设置为None。Python 会将每个节点的引用计数减少到 1。

pythonfor循环语句获取序号_开发语言_03

2、Python 中的零代(Generation Zero)

  • 以上例子中有一个“孤岛”(一组未使用、互相指向的对象,但谁都没有外部引用)。
  • 程序不再使用这些节点对象,所以希望 Python 的垃圾回收机制能足够智能去释放这些对象并回收它们占用的内存空间。但这不可能,因为所有的引用计数都是 1 而不是 0。Python 的引用计数算法不能够处理互相指向自己的对象。
  • 这就是 Python 引入 Generational GC 算法的原因。
  • 正如 Ruby 使用一个链表(free list)来持续追踪未使用、自由的对象一样,Python 使用一种不同的链表(Python 的内部 C 代码将其称为零代,Generation Zero)来持续追踪活跃的对象。每次创建一个对象时,Python 会将其加入零代链表:

pythonfor循环语句获取序号_数据结构_04

  • 当创建ABC节点时,Python 将其加入零代链表(注意:这并不是一个真正的列表,不能直接在代码中访问),事实上这个链表是一个完全内部的 Python 运行时。
  • 类似的,当创建DEF节点时,Python 将其加入同样的链表:

pythonfor循环语句获取序号_pythonfor循环语句获取序号_05

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

3、检测循环引用

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

pythonfor循环语句获取序号_数据结构_06

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

pythonfor循环语句获取序号_Python_07

  • 通过识别内部引用,Python 能减少许多零代链表对象的引用计数。上图第一行中能看见ABCDEF的引用计数已经变为零,这意味着收集器可以释放它们并回收内存空间。剩下的活跃的对象则被移动到一个新的链表:一代链表。
  • 从某种意义上说,Python 的 GC 算法类似于 Ruby 所用的标记回收算法。周期性地从一个对象到另一个对象追踪引用以确定对象是否还活跃、正在被程序所使用,这正类似于 Ruby 的标记过程。

4、Python 中的 GC 阈值

  • Python 什么时候会进行这个标记过程?随着程序运行,Python 解释器保持对新创建的对象、以及因为引用计数为零而被释放掉的对象的追踪。理论上这两个值应该保持一致,因为程序新建的每个对象都应该最终被释放掉。
  • 当然,事实并非如此。因为循环引用和程序使用了一些比其它对象存在时间更长的对象,从而被分配对象的计数值与被释放对象的计数值之间的差异在逐渐增长。一旦这个差异累计超过某个阈值,则 Python 的收集机制就启动了,并触发零代算法,释放“浮动的垃圾”,并将剩下的对象移动到一代列表。
  • 随着时间的推移,程序所使用的对象逐渐从零代列表移动到一代列表。而 Python 对于一代列表中对象的处理遵循同样的方法,一旦被分配计数值与被释放计数值累计到达一定阈值,Python 会将剩下的活跃对象移动到二代列表。
  • 通过这种方法,代码所长期使用的对象、持续访问的活跃对象,会从零代链表转移到一代再转移到二代。通过不同的阈值设置,Python 可以在不同时间间隔处理这些对象。Python 处理零代最为频繁,其次是一代然后才是二代。

5、弱代假说

  • 分代垃圾回收算法的核心:垃圾回收器会更频繁处理新对象。新对象即程序刚刚创建的,而老对象则是经过了几个时间周期之后仍然存在的对象。Python 会在当一个对象从零代移动到一代,或从一代移动到二代的过程中提升(promote)这个对象。
  • 为什么要这么做?这种算法的根源来自于弱代假说(weak generational hypothesis),该假说由两个观点构成:年轻的对象通常死得也快,而老对象则很有可能存活更长的时间。
  • 假定用 Python 或 Ruby 创建一个新对象:

pythonfor循环语句获取序号_数据结构_08

  • 根据假说,代码很可能仅仅会使用ABC很短的时间。这个对象也许仅仅只是一个方法中的中间结果,随着方法的返回这个对象将变成垃圾(大部分的新对象都是如此般地很快变成垃圾)。然而,偶尔程序会创建一些很重要、存活时间比较长的对象(如 web 应用中的 session 变量或配置项)。
  • 通过频繁的处理零代链表中的新对象,Python 的垃圾收集器将把时间花在更有意义的地方:它处理那些很快就可能变成垃圾的新对象。同时只有当满足阈值条件时(不频繁)收集器才会去处理那些老变量。