对象生命周期

本章节介绍了在一个对象的整个生命周期内类型的槽位是如何彼此相互关联的。 在此并不打算做成针对这些槽位的完整规范参考文档;对于此种需求,请参阅 类型对象结构体 中槽位专属文档了解特定槽位的详情。

生命周期事件

下图说明了在对象的整个生命周期中可能发生的事件的顺序。从 AB 的箭头表示事件 B 可以在事件 A 发生之后发生,箭头的标签表示在事件 A 之后发生 B 的条件必须为真。

Life Events tp_new tp_new start->tp_new    type call   tp_alloc tp_alloc tp_new->tp_alloc  direct call   tp_init tp_init tp_new->tp_init reachable reachable tp_init->reachable reachable->tp_init tp_traverse tp_traverse reachable->tp_traverse  not in a    cyclic    isolate   reachable->tp_traverse  periodic    cyclic isolate     detection   finalized? marked as finalized? reachable->finalized?  no refs   tp_finalize tp_finalize reachable->tp_finalize  resurrected    (maybe remove    finalized mark)   uncollectable uncollectable (leaked) reachable->uncollectable  cyclic    isolate    (no GC    support)   tp_dealloc tp_dealloc reachable->tp_dealloc  no refs tp_traverse->finalized?  cyclic    isolate   finalized?->tp_finalize  no (mark    as finalized)   tp_clear tp_clear finalized?->tp_clear  yes   tp_finalize->tp_clear  no refs or     cyclic isolate   tp_finalize->tp_dealloc  recommended  call (see  explanation) tp_finalize->tp_dealloc   no refs   tp_clear->uncollectable  cyclic    isolate   tp_clear->tp_dealloc  no refs   tp_free tp_free tp_dealloc->tp_free    direct call  

解释:

  • 当通过调用其类型构造一个新对象时:

    1. 调用 tp_new 来创建一个新对象。

    2. tp_alloctp_new 直接调用,为新对象分配内存。

    3. tp_init 初始化新创建的对象。 如果需要,可以再次调用 tp_init 来重新初始化对象。 也可以完全跳过 tp_init 调用,例如 Python 代码调用 __new__()

  • tp_init 完成之后,对象就可以使用了。

  • 在最后一个对象的引用被删除后的一段时间:

    1. 如果一个对象没有被标记为 finalized,它可以通过将其标记为 finalized 并调用它的 tp_finalize 函数来终结。 当一个对象的最后一个引用被删除时,Python并 终结它;使用 PyObject_CallFinalizerFromDealloc() 来确保总是调用了 tp_finalize

    2. 如果该对象被标记为已终结,垃圾收集器可能会调用 tp_clear 来清除该对象持有的引用。当该对象的引用计数达到零时, 调用它。

    3. 调用 tp_dealloc 来销毁该对象。 为了避免代码重复,tp_dealloc 通常调用 tp_clear 来释放该对象的引用。

    4. tp_dealloc 完成对象销毁时,它直接调用 tp_free (通常根据类型自动设置为 PyObject_Free()PyObject_GC_Del()) 来释放内存。

  • tp_finalize 函数被允许在需要时增加对象的引用计数。 如果该函数执行此操作,那么对象将被 复活,从而阻止其即将发生的销毁。 只有 tp_finalize 能够复活对象;tp_cleartp_dealloc 在不调用 tp_finalize 的情况下不能实现此操作。 对象复活后,其 已终结 标记可能被移除也可能保留。 当前 Python 的实现逻辑是:如果对象支持垃圾回收(即设置了 Py_TPFLAGS_HAVE_GC 标志位),那么保留复活对象的 已终结 标记;如果不支持垃圾回收,那么移除该标记。 此行为在未来版本中可能发生变更。

  • tp_dealloc 可以通过 PyObject_CallFinalizerFromDealloc() 选择性地调用 tp_finalize,如果它希望重用该代码来帮助对象销毁。 建议这样做,因为它保证总是在销毁之前调用:c:member:!tp_finalize。 请参阅 tp_dealloc 文档获取示例代码。

  • 如果对象是 cyclic isolate 的成员,并且 tp_clear 未能打破循环引用或未检测到循环隔离(可能调用了 gc.disable(),或者在所涉及的类型之一中错误地省略了 Py_TPFLAGS_HAVE_GC 标志),则对象将无限期地保持不可收集(它们“泄漏”)。 参见 gc.garbage

如果对象被标记为支持垃圾收集(在 tp_flags 中设置了 Py_TPFLAGS_HAVE_GC 标志),则也可能发生以下事件:

  • 垃圾回收器偶尔会调用 tp_traverse 来识别 循环隔离

  • 当垃圾回收器发现一个 cyclic isolate 时,它通过将组中的一个对象标记为 finalized 并调用其 tp_finalize 函数(如果有的话)来终结该对象。 这样重复,直到循环隔离不存在或所有对象都已终结。

  • 允许 tp_finalize 通过添加来自 cyclic isolate 外部的引用来复活对象。 新的引用导致对象组不再形成循环隔离(循环引用可能仍然存在,但如果存在,则对象不再被隔离)。

  • 当垃圾回收器发现 cyclic isolate 并且组中的所有对象已经被标记为 finalized 时,垃圾回收器通过调用每个对象的 tp_clear 函数来清除组中一个或多个未清除的对象(可能并发)。 只要循环隔离仍然存在并且未清除所有对象,就会重复此操作。

循环隔离销毁

以下描述了一个假设性的 cyclic isolate 在其每个成员对象都被终结或清除后仍能持续存在。 如果一个循环隔离体完整经历了所有这些阶段而未消失则构成内存泄漏。 正常情况下当所有对象被清除或是更早时循环隔离体就应当消失。 循环隔离体的消失可能是由于引用循环被打破或是因为对象因终结器复活而不再处于隔离状态 (参见 tp_finalize)。

  1. 可获取 (尚未循环隔离):所有对象均处于正常的可获取状态。 可能存在引用循环,但有外部引用意味着对象还未被隔离。

  2. 不可达但一致: 来自循环对象组外部的最后引用已被删除,导致对象被隔离(因此产生了循环隔离)。该组的所有对象还没有终结或清除。循环隔离一直保持在这个阶段,直到垃圾回收器的某个未来运行(不一定是下一次运行,因为下一次运行可能不会扫描每个对象)。

  3. 已终结和未终结对象的混合情况: 在循环隔离组(cyclic isolate)中,对象的终结是逐个进行的。这意味着会存在一个阶段,循环隔离组中同时包含已终结(finalized)和未终结(non-finalized)的对象。由于终结顺序是不确定的,其表现可能看似随机。已终结对象在被未终结对象交互时应当保持合理行为,未终结对象应当能够容忍其任意部分引用对象被终结。

  4. 已全部终结: 循环隔离中的所有对象在清除它们之前都已终结。

  5. 已终结和已清除的混合: 对象可以被串行或并发地清除 (但持有 GIL);不管怎样,有些对象会比其他对象先完成。 一个已终结对象必须能够容忍其部分引用对象被清除。 PEP 442 称这个阶段为“循环垃圾”。

  6. 泄漏: 如果在组中的所有对象都已终结并清除后循环隔离仍然存在,则对象仍然无限期不可 lknhy (参见 gc.garbage)。 如果循环隔离达到这个阶段,这是一个 bug —— 这意味着参与对象的 tp_clear 方法未能按要求打破循环引用。

如果类型对象中的 tp_clear 成员不存在,那么Python 将无法安全地打破引用循环(reference cycle)。如果直接销毁循环孤立组中的对象,那么会导致悬垂指针(dangling pointer),当其他对象引用该已销毁对象时,会引发未定义行为(undefined behavior)。 清除(clearing)操作将对象销毁分为两个阶段: 首先调用 tp_clear 部分销毁对象,解除循环引用; 随后调用 tp_dealloc 完成最终销毁。

与清除不同的是,终结不是一个破坏阶段。 一个已终结的对象必须通过继续履行其设计契约而保持正确的行为。 一个对象的终结器允许执行任意 Python 代码,甚至允许通过添加引用来防止即将发生的销毁。 终结器只通过调用顺序与销毁相关 —— 如果它运行,它在销毁之前运行,销毁以 tp_clear (如果被调用) 开始,并以 tp_dealloc 结束。

对于安全回收循环隔离体中的对象而言,终结步骤并非必要,但是,该机制的存在能让类型设计更易于实现合理的清理行为。清除对象时或许不可避免地会使其处于损坏的、部分销毁的状态——此时调用被清除对象的任何方法或访问其属性都可能是不安全的。通过终结机制,只有已完成终结的对象才有可能与被清除对象交互;未终结的对象则保证只会与未被清除(但可能已完成终结)的对象进行交互。

总结一下可能的交互:

  • 一个未终结的对象可能有对(或来自)未终结和已终结对象的引用,但不能有对(或来自)已清除对象的引用。

  • 一个已终结的对象可能有对(或来自)未终结、已终结和已清除对象的引用。

  • 一个已清除的对象可能有对(或来自)已终结和已清除对象的引用,但不能有对(或来自)未终结对象的引用。

在没有引用循环的情况下,对象只需在最后一个引用被删除时即可直接销毁,此时无需执行最终化(finalize)和清理(clear)步骤也能安全回收对象。即便如此,在销毁前自动调用 tp_finalizetp_clear 仍具有实际意义:这能统一所有对象的销毁流程,无论它们是否参与过循环引用隔离区(cyclic isolate),类型设计会更简洁。当前 Python 仅在需要销毁循环引用隔离区时才调用 tp_finalizetp_clear。未来版本可能调整这一行为。

函数

要分配和释放内存,请参阅 在堆上分配对象

void PyObject_CallFinalizer(PyObject *op)

按照 tp_finalize 中描述的方式终结对象。 调用这个函数 (或 PyObject_CallFinalizerFromDealloc()),而不是直接调用 tp_finalize,因为这个函数可能会重复多次调用 tp_finalize。 目前,只有当类型支持垃圾回收(即设置了 Py_TPFLAGS_HAVE_GC 标志)时,才会重复调用;这在未来可能会改变。

int PyObject_CallFinalizerFromDealloc(PyObject *op)

PyObject_CallFinalizer() 相同但确定要在对象构造器的开始位置被调用 (tp_dealloc)。 必须没有任何指向对象的引用。 如果对象的终结器复活了该对象,此函数将返回 -1;不会再发生销毁操作。 在其他情况下,此函数将返回 0 并且销毁操作可正常继续。

参见

tp_dealloc 用于示例代码。