10. 垃圾收集器

CPython 使用引用计数作为其主要内存管理机制。一旦最后一个强引用消失,引用计数就会销毁大多数对象。

引用计数有一个主要限制:它无法自行回收引用循环。

垃圾收集器的存在是为了找到容器对象的不可达循环并回收它们。它是引用计数的补充,而不是替代。

10.1 为什么引用计数需要帮助

仅当没有强引用指向对象时,引用计数才会达到零。

这适用于普通对象图:```python x = [] del x


它不适用于循环:```python
a = []
b = []

a.append(b)
b.append(a)

del a
del b
```删除两个名称后,列表仍然相互引用:```text
list A ---> list B
list B ---> list A
```它们的引用计数保持非零。但实时 Python 代码无法访问它们。

引用计数看到本地所有权。垃圾收集看到了可达性。

## 10.2 垃圾收集器跟踪什么

CPython 不需要跟踪循环垃圾收集器中的每个对象。

不能包含对其他 Python 对象的引用的对象不能自行形成循环。示例包括许多整数、浮点数和简单字符串。

收集器主要跟踪类似容器的对象:```text
list
dict
set
tuple containing references
function
class
instance
frame
generator
coroutine
traceback
some extension objects
```当一个对象可以参与循环时,就需要GC跟踪。

例如:```python
x = 123
```整数对象不会以创建容器循环的方式指向其他 Python 对象。

但:```python
x = []
x.append(x)
```该列表指向其自身。这是一个循环。

## 10.3 自循环

最简单的引用循环是自循环:```python
x = []
x.append(x)
```结构是:```text
x ---> list
       ^  |
       |  |
       +--+
```现在删除外部名称:```python
del x
```该列表仍然包含对其自身的引用。它的引用计数保持在零以上。

任何程序变量都无法到达它,但仅引用计数无法破坏它。循环垃圾收集器必须检测到它。

## 10.4 多对象循环

循环通常涉及多个对象。```python
class Node:
    def __init__(self):
        self.parent = None
        self.children = []

root = Node()
child = Node()

root.children.append(child)
child.parent = root

del root
del child
```该图变得无法访问,但引用仍保留在其中:```text
root node ---> children list ---> child node
    ^                              |
    |                              |
    +----------- parent -----------+
```这种模式在树、图、对象模型、ASTDOM、缓存、框架、闭包和异常回溯中很常见。

## 10.5 收集器在容器上工作

循环收集器只需要推理可以指向其他对象的对象。

它询问每个被跟踪的对象:```text
Which Python objects do you reference?
``` C 级别,扩展类型通过遍历支持进行应答。 GC 感知类型提供了访问包含的 Python 引用的遍历函数。

从概念上讲:```c
static int
Node_traverse(NodeObject *self, visitproc visit, void *arg)
{
    Py_VISIT(self->parent);
    Py_VISIT(self->children);
    return 0;
}
```收集器使用它来遍历对象图。

如果扩展对象拥有 Python 引用并且可以成为循环的一部分,则它必须参与此协议。否则,可能会泄漏循环。

## 10.6 跟踪和未跟踪的对象

循环收集器可以跟踪或取消跟踪对象。

跟踪意味着收集者可以在收集过程中检查它。

Untracked 意味着收集器在循环检测时忽略它。

您可以从 Python 中检查这一点:```python
import gc

print(gc.is_tracked([]))
print(gc.is_tracked(123))
print(gc.is_tracked("hello"))
```CPython 上的典型输出可能显示容器被跟踪,而原子对象则未被跟踪。由于 CPython 应用了优化,确切的结果可能会有所不同。例如,某些容器在仅包含原子对象时可能会变得不受跟踪。

重要的规则是概念性的:```text
objects that can participate in cycles need tracking
objects that cannot participate in cycles usually do not
```## 10.7 分代收集

CPython 的循环垃圾收集器是分代的。

这个想法基于一个常见的观察:大多数物体都会在年轻时死亡。在多次收集中幸存下来的对象可能寿命更长。

分代收集器按年龄对跟踪的对象进行分组。年轻一代的收集频率更高。老一代的收集频率较低。

从概念上讲:```text
generation 0
    newest tracked objects
    collected most often

generation 1
    objects that survived earlier collection
    collected less often

generation 2
    older tracked objects
    collected least often
```确切的生成设计可能会因 CPython 版本而异。有用的心理模型是 CPython 避免扫描每个集合上的所有跟踪容器。

## 10.8 收集阈值

`gc`模块公开阈值:```python
import gc

print(gc.get_threshold())
```阈值有助于决定自动循环收集何时运行。

您可以更改它们:```python
gc.set_threshold(700, 10, 10)
```您可以强制收集:```python
gc.collect()
```您可以禁用自动循环收集:```python
gc.disable()
```并重新启用它:```python
gc.enable()
```禁用收集器不会禁用引用计数。引用计数达到零的对象仍然会被销毁。禁用收集器只会禁用自动循环检测。

## 10.9 循环检测的工作原理

收集器不只是寻找具有非零引用计数的对象。几乎每个活动对象都有一个非零引用计数。

相反,它计算使组保持活动状态的引用是否仅来自该组内部。

简化的循环检测过程:```text
select tracked candidate objects
copy each object's reference count into a temporary field
for each reference from candidate object to candidate object:
    subtract one from the target's temporary count
objects with temporary count still positive are reachable from outside
propagate reachability from those externally reachable objects
objects never reached are unreachable cycles
```例子:```text
outside ---> A ---> B
             ^     |
             |     v
             D <--- C
```即使`A`, `B`, `C` `D`形成一个循环,外部参考`A`使整个团体都可以到达。

但:```text
A ---> B
^     |
|     v
D <--- C
```没有外部参考是可以收藏的。

## 10.10 可到达的周期不是垃圾

循环不会自动成为垃圾。```python
a = []
a.append(a)

print(a)
```该对象是循环的,但可以通过名称访问`a`。它必须活着。

收集器仅回收无法到达的周期。

这种区别对于数据结构很重要。循环图在 Python 中是正常且有效的。收集器的存在是为了在常见情况下可以安全地使用它们而无需手动破坏。

## 10.11 终结器

终结器使垃圾收集变得复杂。

终结器通常是`__del__`方法:```python
class Resource:
    def __del__(self):
        print("destroying")
```终结器在对象销毁期间运行。他们可以执行Python代码。该代码可以访问全局变量、改变状态、获取锁、创建对象,甚至复活正在终结的对象。

对象复活意味着终结器使对象再次可访问:```python
saved = None

class Resurrect:
    def __del__(self):
        global saved
        saved = self
```这使得收集变得更加复杂。

现代 CPython 对于最终确定循环垃圾有特定的规则,但实际指导很简单:```text
avoid complex __del__ methods
prefer context managers
prefer weakref.finalize for cleanup hooks
```## 10.12 上下文管理器更适合资源

垃圾收集不是资源管理 API

使用`with`对于确定性清理:```python
with open("data.txt") as f:
    data = f.read()
```当块退出时,这将关闭文件。

不要依赖垃圾收集计时:```python
f = open("data.txt")
data = f.read()
f = None
``` CPython 中,引用计数可能会快速关闭文件。在其他实现中,清理可能稍后发生。

对于锁、套接字、文件、事务、临时目录和外部句柄,请使用显式生命周期控制。

## 10.13`gc.collect`

`gc.collect()`强制循环收集。```python
import gc

n = gc.collect()
print(n)
```返回值是找到并收集的不可达对象的数量。

您可以请求支持该接口的特定版本版本:```python
gc.collect(0)
gc.collect(1)
gc.collect(2)
```手动收集适用于:```text
tests
debugging leaks
interactive experiments
memory-sensitive batch phases
controlled benchmarks
```普通应用程序代码中很少需要它。

## 10.14`gc.get_objects`

`gc.get_objects()`返回收集器已知的跟踪对象。```python
import gc

objs = gc.get_objects()
print(len(objs))
```这不会返回每个活动的 Python 对象。它返回循环垃圾收集器跟踪的对象。

许多原子对象可能不存在。

使用它来调试对象图和内存泄漏,而不是用于正常的应用程序逻辑。

## 10.15`gc.get_referrers`

`gc.get_referrers(obj)`返回直接引用的对象`obj````python
import gc

x = []
refs = gc.get_referrers(x)
print(refs)
```这可以帮助解释为什么一个物体仍然活着。

但必须谨慎使用。调用调试函数会创建临时引用和框架。这些可能会出现在结果中。`gc.get_referrers`还公开了实现细节。它可以显示堆栈帧、字典、列表和内部对象。

## 10.16`gc.get_referents`

`gc.get_referents(obj)`返回直接引用的对象`obj````python
import gc

x = [[1], [2]]
print(gc.get_referents(x))
```对于列表,这包括其元素。

对于字典,这可能包括键和值。

此函数使用与收集器相同的遍历支持。它看到的是 GC 级别的引用对象,而不一定是程序员可能想象的所有语义关系。

## 10.17 无法收集的垃圾

某些对象可能无法访问,但在某些条件下不能立即收集。

从历史上看,包含终结器的循环尤其困难。现代 CPython 可以更好地处理许多此类情况,但不可收集的对象仍然可能会以扩展类型或异常的终结行为出现。

`gc`模块暴露:```python
import gc

print(gc.garbage)
```调试时,您可以启用调试标志:```python
gc.set_debug(gc.DEBUG_SAVEALL)
````DEBUG_SAVEALL`,无法访问的对象保存在`gc.garbage`而不是被释放。这对于检查很有用,但它会故意泄漏,直到您清除列表为止。

## 10.18 弱引用

弱引用允许观察对象而不使其保持活动状态。```python
import weakref

class User:
    pass

u = User()
r = weakref.ref(u)

print(r())      # object

del u

print(r())      # None
```弱引用不会增加目标的强引用计数。

弱引用对于缓存、观察者列表、父链接和辅助元数据很有用。

父指针通常可能很弱:```python
import weakref

class Node:
    def __init__(self):
        self.children = []
        self.parent = None

root = Node()
child = Node()

child.parent = weakref.ref(root)
root.children.append(child)
```这避免了通过父子链接形成强循环。

## 10.19`weakref.finalize`

`weakref.finalize`注册清理代码,而不直接将清理逻辑放入`__del__````python
import weakref

class Resource:
    pass

def cleanup(name):
    print("cleaning", name)

r = Resource()
finalizer = weakref.finalize(r, cleanup, "resource")
```什么时候`r`变得无法访问,终结器可以运行。

这通常比编写复杂的代码更安全`__del__`方法,因为终结器将清理状态与正在终结的对象分开。

尽管如此,如果可能的话,外部资源通常应该由上下文管理器来管理。

## 10.20 帧、回溯和循环

帧和回溯通常会产生循环。

例子:```python
def f():
    x = []
    raise RuntimeError

try:
    f()
except RuntimeError as exc:
    saved = exc
```异常可以保留回溯。回溯可以保存帧。框架保存局部变量。局部变量可以保存异常或与之相关的对象。

从概念上讲:```text
exception
    traceback
        frame
            locals
                exception
```这可以使大型对象图保持活动状态。

现代 Python 比旧版本更积极地清除某些异常状态,但存储的异常和回溯仍然可以保留内存。

常见缓解措施:```python
try:
    ...
except Exception as exc:
    ...
finally:
    exc = None
```或者避免存储回溯超过必要的时间。

## 10.21 闭包和循环

闭包也会产生循环。```python
def make_func():
    items = []

    def add(x):
        items.append(x)
        return items

    return add
```内部函数引用一个单元格。单元格引用`items`

更复杂的闭包可以引用回引用该函数的对象。

函数、闭包、实例和回调的循环在实际程序中很常见。

收集器处理其中的许多问题,但是当内存意外增长时,理解图表会有所帮助。

## 10.22 扩展类型和 GC 支持

拥有对 Python 对象的引用的 C 扩展类型可能需要循环 GC 支持。

所需的部分通常包括:```text
Py_TPFLAGS_HAVE_GC
tp_traverse
tp_clear
GC-aware allocation
GC-aware deallocation
```遍历函数告诉收集器该对象引用了哪些对象:```c
static int
Box_traverse(BoxObject *self, visitproc visit, void *arg)
{
    Py_VISIT(self->value);
    return 0;
}
```一个clear函数在收集过程中会破坏引用:```c
static int
Box_clear(BoxObject *self)
{
    Py_CLEAR(self->value);
    return 0;
}
```释放器必须取消跟踪对象并安全地清除拥有的引用。

概念形状:```c
static void
Box_dealloc(BoxObject *self)
{
    PyObject_GC_UnTrack(self);
    Box_clear(self);
    Py_TYPE(self)->tp_free((PyObject *)self);
}
```这是简化的。真正的扩展代码必须遵循确切的 C API 要求。

## 10.23`Py_VISIT`

`Py_VISIT`用于遍历函数内部。```c
static int
Node_traverse(NodeObject *self, visitproc visit, void *arg)
{
    Py_VISIT(self->left);
    Py_VISIT(self->right);
    Py_VISIT(self->parent);
    return 0;
}
```它检查该字段是否为非 NULL 并将其传递给访问者。

正确的遍历函数必须访问每个包含的可以参与循环的 Python 对象引用。

缺少一个字段可能会使收集器看不到循环。

## 10.24`Py_CLEAR`

`Py_CLEAR`安全地清除拥有的引用。

A naive clear operation:```c
Py_DECREF(self->value);
self->value = NULL;
```有一个微妙的问题。这`Py_DECREF`可以通过终结器运行任意代码。 That code might observe`self``self->value`已设置为`NULL``Py_CLEAR`将字段设置为`NULL`首先,然后递减旧的引用。

从概念上讲:```c
tmp = self->value;
self->value = NULL;
Py_XDECREF(tmp);
```使用`Py_CLEAR`当破坏容器或解除分配器内的引用时。

## 10.25 Collection and Performance

循环收集器是有成本的。它扫描跟踪的容器并跟踪引用。

Most programs should leave it enabled.但某些工作负载可能会对其进行调整:```text
short-lived batch jobs
allocation-heavy parsers
large object graph construction
scientific pipelines
services with known allocation phases
```例子:```python
import gc

gc.disable()
try:
    build_large_graph()
finally:
    gc.enable()
    gc.collect()
```当您知道一个阶段创建了许多没有循环的容器时,此模式会有所帮助。如果周期累积也会造成伤害。

调谐前进行测量。

## 10.26 内存泄漏与保留引用

并非所有内存增长都是 C 意义上的泄漏。

程序可能会意外保留引用:```python
cache = []

def handle(request):
    cache.append(request)
```收集器无法释放可到达的对象。如果`cache`不断增长,那些物体是活的。

真正的 C 级泄漏意味着内存或引用因实现而丢失。

Python 级别的保留错误意味着程序仍然具有对其不再需要的对象的可访问引用。

调试内存通常从询问以下问题开始:```text
Is the object unreachable but not collected?
Or is something still referring to it?
```使用`gc.get_referrers`tracemalloc、对象图工具和堆检查来回答这个问题。

## 10.27 常见循环模式

常见的循环来源包括:

|图案|形状|
| ---------------------- | ------------------------------------------------ |
|家长与孩子的联系 |父级 -> 子级 -> 父级 |
|双向链表|节点 A -> 节点 B -> 节点 A |
|图形结构|任意循环 |
|观察者回调 |对象 -> 回调 -> 绑定方法 -> 对象 |
|例外 |异常 -> 回溯 -> 框架 -> 本地 |
|关闭|函数 -> 闭包单元 -> 对象 -> 函数 |
|描述符| -> 描述符 -> 类相关状态 |
| C 扩展对象 |本机对象 -> Python 对象 -> 本机包装 |

周期正常。问题在于无法访问的循环是否可收集,以及它们是否包含需要确定性清理的外部资源。

## 10.28 实用规则

对于Python代码:

|情况|首选方法 |
| ---------------------- | -------------------------------------------------- |
|外部资源|使用`with`|
|父指针|考虑`weakref`|
|缓存|使用`weakref.WeakValueDictionary`在适当的时候|
|长期异常 |避免不必要地保留回溯 |
|内存调试|使用`gc`, `tracemalloc`,和推荐人检查|
|清理钩|更喜欢上下文管理器或`weakref.finalize`|
|大额分配阶段|仅在测量后调整 GC |

对于 C 扩展代码:

|情况|所需纪律 |
| ------------------------------------------- | --------------------------------- |
|类型拥有 Python 引用 |实施适当的重新分配 |
|类型可以形成循环 |添加 GC 支持 |
|类型支持GC |实施`tp_traverse`正确|
|类型参与征集|实施`tp_clear`正确|
|突破参考|使用`Py_CLEAR`|
|穿越田野 |使用`Py_VISIT`|
|释放 GC 对象 |清除前取消追踪 |

## 10.29 心智模型

使用这个模型:```text
Reference counting handles local lifetime.
Cyclic GC handles unreachable container cycles.
The collector only sees tracked objects.
Tracked objects must describe their outgoing references.
Reachable cycles remain alive.
Unreachable cycles can be collected.
Finalizers and extension types complicate collection.
Resource lifetime should be explicit.
```这个模型解释了为什么大多数 CPython 对象会立即消失,为什么一些循环结构会一直存在直到集合,为什么`__del__`需要注意,以及为什么 C 扩展类型在拥有 Python 引用时必须参与 GC 协议。

## 10.30 总结

CPython 的垃圾收集器之所以存在,是因为引用计数无法回收循环。它跟踪类似容器的对象,分析它们之间的引用,找到无法访问的组,并安全地清除它们。

收集器是分代的,可通过以下方式配置`gc`模块,并与对象模型紧密相关。 Python程序员主要需要了解循环、终结器、弱引用和资源生命周期。当 C 扩展作者的类型可以参与循环时,必须正确实现遍历和清除。