Python 3.14 垃圾回收的波折

Hacker News Top 新闻

摘要

Python 3.14 引入了一个增量垃圾回收器,但由于内存压力报告,该回收器在 3.14.5 中被回滚。本文解释了这些变化、它们的影响以及围绕回滚的争议。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/06/14 07:37

# Python 3.14 垃圾回收折腾记 原文链接:https://theconsensus.dev/p/2026/06/06/python-3-14-garbage-collection-rigamarole.html The Consensus 标志 (https://theconsensus.dev/) 关于软件基础设施。 ## Python 3.14 垃圾回收折腾记 Python 3.14.0 引入了一个新的增量垃圾回收器。但由于内存使用率升高的报告,Python 团队在 3.14.5 中回退了垃圾回收器的改动。 我们研究一下 Python 的内存管理方式,以及增量垃圾回收器在哪些工作负载下表现最佳和最差。 作者:Phil Eaton · 2026 年 6 月 6 日 您作为订阅者正在提前阅读本文。您的支持使此类文章成为可能。谢谢。 Python (https://theconsensus.dev/project/python.html) 3.14.5 (https://blog.python.org/2026/05/python-3145-is-out/?from_theconsensus=1) 刚刚发布,是当前最新的 Python 稳定版本。 Python 3.14.0(2025 年 10 月发布)将垃圾回收器(GC)从传统分代垃圾回收改为增量垃圾回收。我们将在本文中详细解释这意味着什么。 根据将这个改动合并到 Python (https://theconsensus.dev/project/python.html) 的拉取请求 (https://github.com/python/cpython/pull/116206?from_theconsensus=1): > 循环垃圾回收器现在变成增量式的。这意味着对于较大的堆,最大暂停时间减少了一个数量级或更多。 > 现在只有两代:年轻代和老年代。当没有直接调用 gc.collect() 时,GC 调用的频率略有降低。当被调用时,它会收集年轻代和一部分老年代,而不是收集一代或多代。 这个拉取请求在 2024 年就进入了 Python (https://theconsensus.dev/project/python.html) 的主分支,但被从 3.13 发布分支 (https://github.com/python/cpython/pull/124770?from_theconsensus=1) 中移除。 Python 3.14.0 是第一个包含此改动的版本。但用户报告 (https://github.com/python/cpython/issues/142516?from_theconsensus=1) 了“内存压力”,所以 Python (https://theconsensus.dev/project/python.html) 团队在 3.14.5 发布版中回退了垃圾回收器的改动。我们也会在本文中解释“内存压力”的含义。 遗憾的是,垃圾回收的改动在某种程度上是有意 (https://discuss.python.org/t/reverting-the-incremental-gc-in-python-3-14-and-3-15/107014/35?from_theconsensus=1)(如果这个词不过分的话)不实现为可供用户切换的选项(例如,在 Java (https://inside.java/2023/11/28/gen-zgc-explainer/?from_theconsensus=1#using-zgc) 或 Go (https://go.dev/blog/greenteagc?from_theconsensus=1) 中可以做到)。喜欢新增量垃圾回收器(而且确实有人喜欢 (https://discuss.python.org/t/reverting-the-incremental-gc-in-python-3-14-and-3-15/107014/59?from_theconsensus=1))的用户再也无法使用它了。 同样有趣的是,GC 改动一开始就没有遵循常规的 PEP 流程 (https://peps.python.org/pep-0001/?from_theconsensus=1)。 要理解这一切意味着什么,我们需要从 GC 之前开始讲起:从引用计数开始。 *在整篇文章中,当我只讨论“CPython”时,我会说“Python (https://theconsensus.dev/project/python.html)”。* ## Python (https://theconsensus.dev/p/2026/06/06/python-3-14-garbage-collection-rigamarole.html#python) 在进入引用计数之前,我们本地构建两个 Python (https://theconsensus.dev/project/python.html) 版本:3.14.4 和 3.14.5。由于我们无法在同一个版本内切换 GC,我们所能做的最好(正如我们在整篇文章中演示行为那样)是在这两个补丁版本之间切换。3.14.4 使用增量 GC,3.14.5 使用传统 GC。 ``` sudo apt-get update -y sudo apt-get install -y build-essential pkg-config git clone --depth 1 --branch v3.14.4 https://github.com/python/cpython cpython3.14.4 git clone --depth 1 --branch v3.14.5 https://github.com/python/cpython cpython3.14.5 (cd cpython3.14.4 && ./configure --with-trace-refs && make -j16) (cd cpython3.14.5 && ./configure --with-trace-refs && make -j16) ``` `--with-trace-refs` 选项启用了我们稍后要讨论的一个额外调试方法。 现在你有了两个版本。 ``` $ ./cpython3.14.4/python --version Python 3.14.4 $ ./cpython3.14.5/python --version Python 3.14.5 ``` 让我们开始了解内存管理吧! ## 引用计数入门 (https://theconsensus.dev/p/2026/06/06/python-3-14-garbage-collection-rigamarole.html#reference-counting-primer) Python (https://theconsensus.dev/project/python.html) 中的对象是引用计数的。对对象的新引用会增加计数。计数在几种情况下会减少。例如:当变量超出作用域、变量被 `del` 删除、或变量被绑定到另一个对象时。 我们可以通过 `sys.getrefcount` 观察引用计数。 ``` import sys print(sys.getrefcount([])) # 1 ``` refcount1.py 运行它。 ``` $ ./cpython3.14.4/python refcount1.py 1 $ ./cpython3.14.5/python refcount1.py 1 ``` 在这个例子中,我们创建了一个新对象,并且只有单个引用。一旦 `sys.getrefcount()` 完成,它就会被释放,因为其引用计数降为 0。 变量会创建额外的引用。 ``` import sys a = [] # 这个对象的 refcount 是 1 print(sys.getrefcount(a)) # 2: `a` 和传递给 `sys.getrefcount` 的临时引用 ``` refcount2.py 运行它。 ``` $ ./cpython3.14.4/python refcount2.py 2 $ ./cpython3.14.5/python refcount2.py 2 ``` 指向同一个对象的多个变量会创建多个引用。我们可以打印指向同一个对象的多个变量的 `id(obj)`(在 CPython 中实际上是对象的内存地址),并观察到 id 是相同的。Python (https://theconsensus.dev/project/python.html) 的引用计数作用于对象,而不是变量。 ``` import sys a = [] print("a memory", hex(id(a))) # 在我的机器上运行时为 0x1026e1680 # 0x1026e1680 的 refcount 是 1: `a` print(sys.getrefcount(a)) # 2: `a` 本身和 sys.getrefcount 的参数 b = a print("b memory", hex(id(b))) # 与 `a memory` 相同,同一运行中为 0x1026e1680 # 0x1026e1680 的 refcount 是 2: `a` 和 `b` print(sys.getrefcount(a)) # 3: `a` 本身、`b` 和作为参数的对象 print(sys.getrefcount(b)) # 3: `b` 本身、`a` 和作为参数的对象 del b # 0x1026e1680 的 refcount 是 1 print(sys.getrefcount(a)) # 2: `a` 本身和作为参数的对象 del a # 0x1026e1680 的 refcount 是 0,被删除 ``` refcount3.py 运行它。 ``` $ ./cpython3.14.4/python refcount3.py a memory 0xfed730bdda40 2 b memory 0xfed730bdda40 3 3 2 $ ./cpython3.14.5/python refcount3.py a memory 0xf1331f9dda40 2 b memory 0xf1331f9dda40 3 3 2 ``` 我们无法观察到大多数内置对象(例如列表、字典等)的释放,但可以通过实现 `__del__` 方法或使用 `weakref.finalize` 分配回调来观察用户对象的释放。 ``` import sys, weakref class Obj: pass a = Obj() weakref.finalize(a, print, "freeing "+hex(id(a))) print("a memory", hex(id(a))) # 在我的机器上运行时为 0x1005b4d70 # 0x1005b4d70 的 refcount 是 1: `a` print(sys.getrefcount(a)) # 2: `a` 本身和 sys.getrefcount 的参数 b = a print("b memory", hex(id(b))) # 与 `a memory` 相同,同一运行中为 0x1005b4d70 # 0x1005b4d70 的 refcount 是 2: `a` 和 `b` print(sys.getrefcount(a)) # 3: `a` 本身、`b` 和作为参数的对象 print(sys.getrefcount(b)) # 3: `b` 本身、`a` 和作为参数的对象 del b # 0x1005b4d70 的 refcount 是 1 print(sys.getrefcount(a)) # 2: `a` 本身和作为参数的对象 del a # 0x1005b4d70 的 refcount 是 0,被删除,观察到打印了 `freeing 0x1005b4d70` ``` refcount4.py 运行它。 ``` $ ./cpython3.14.4/python refcount4.py a memory 0xe496a9bc5160 2 b memory 0xe496a9bc5160 3 3 2 freeing 0xe496a9bc5160 $ ./cpython3.14.5/python refcount4.py a memory 0xfdfcf15b1160 2 b memory 0xfdfcf15b1160 3 3 2 freeing 0xfdfcf15b1160 ``` 当存在循环引用时,这一切都会失效。 ## 引用循环 (https://theconsensus.dev/p/2026/06/06/python-3-14-garbage-collection-rigamarole.html#reference-cycles) 引用计数是一种局部算法,不了解其他对象。所以,虽然自动引用计数通常可以在作用域完成或我们调用 `del` 时,将引用计数递减到零(从而允许对象被释放),但当涉及循环引用时,自动引用计数无法将计数递减到零。 我们将通过发现对象仍然存在于 `sys.getobjects(limit)` (https://docs.python.org/3/library/sys.html?from_theconsensus=1#sys.getobjects) 中来观察这一点,该函数返回所有已分配对象的列表(仅在这些 `--with-trace-refs` 构建中可用)。即使我们对对象调用了 `del`,它仍然会在这个列表中,因为我们的对象包含一个循环引用,使得引用计数无法自行归零。 ``` import sys class Obj: pass a = Obj() # 对 Obj() 的 1 个引用 i = id(a) a.me = a # 对 Obj() 的 2 个引用 assert any(id(o) == i for o in sys.getobjects(0)) del a # 对 Obj() 的 1 个引用 assert any(id(o) == i for o in sys.getobjects(0)) ``` refcount5.py 运行它。 ``` $ ./cpython3.14.4/python refcount5.py $ ./cpython3.14.5/python refcount5.py ``` 很容易把 `del` 误解为释放对象的方法(如果是这样:管它是否引用自身,我们都能删除它,对吧?)。但 `del` 所做的只是从作用域中移除名称 `a` 并递减它指向对象的引用计数。对象仍然存在,并且仍然有对自身的引用,但我们已失去了所有与对象的绑定。这是内存泄漏!或者本来会是。 手动打破引用计数循环的一种方法是使用弱引用。 ``` import sys, weakref class Obj: pass a = Obj() # 对 Obj() 的 1 个引用 i = id(a) a.me = weakref.ref(a) # 仍然是对 Obj() 的 1 个引用 assert any(id(o) == i for o in sys.getobjects(0)) del a # 对 Obj() 的 1 个引用 assert not any(id(o) == i for o in sys.getobjects(0)) ``` refcount6.py 运行它。 ``` $ ./cpython3.14.4/python refcount6.py $ ./cpython3.14.5/python refcount6.py ``` `a.me` 现在是对该对象的弱引用。所以最后的断言反转了,一切工作正常。`a` 所指向的对象已通过引用计数机制释放了。 出于某种原因,这个程序在 3.14.4 和 3.14.5 中都会周期性地但可靠地发生段错误。 ``` #0 free_object (obj=0x7dbdbb11bbf2) at Objects/object.c:921 #1 clear_freelist (dofree=<optimized out>, is_finalization=<optimized out>, freelist=<optimized out>) at Objects/object.c:907 #2 _PyObject_ClearFreeLists (freelists=0x592f73db8e30 <_PyRuntime+101872>, is_finalization=is_finalization@entry=0) at Objects/object.c:952 #3 0x0000592f73a898e2 in _PyGC_ClearAllFreeLists (interp=<optimized out>) at Python/gc_gil.c:14 #4 0x0000592f73a88791 in gc_collect_main (tstate=tstate@entry=0x592f73ded168 <_PyRuntime+315688>, generation=<optimized out>, generation@entry=2, reason=reason@entry=_Py_GC_REASON_MANUAL) at Python/gc.c:1495 #5 0x0000592f73a88e60 in PyGC_Collect () at Python/gc.c:1682 #6 0x0000592f73ac2d29 in _Py_Finalize (runtime=0x592f73da0040 <_PyRuntime>) at Python/pylifecycle.c:2140 #7 0x0000592f73ac30cd in _Py_Finalize (runtime=0x592f73da0040 <_PyRuntime>) at Python/pylifecycle.c:2268 #8 0x0000592f73afbc1d in Py_RunMain () at Modules/main.c:778 #9 pymain_main (args=0x7ffdbcb39900) at Modules/main.c:806 #10 Py_BytesMain (argc=<optimized out>, argv=<optimized out>) at Modules/main.c:830 #11 0x00007dbdbb22a1ca in __libc_start_call_main (main=main@entry=0x592f73870320 <main>, argc=argc@entry=2, argv=argv@entry=0x7ffdbcb39a98) at ../sysdeps/nptl/libc_start_call_main.h:58 #12 0x00007dbdbb22a28b in __libc_start_main_impl (main=0x592f73870320 <main>, argc=2, argv=0x7ffdbcb39a98, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffdbcb39a88) at ../csu/libc-start.c:360 #13 0x0000592f738827d5 in _start () ``` 我打算忽略这个问题。 ## 分代垃圾回收 (https://theconsensus.dev/p/2026/06/06/python-3-14-garbage-collection-rigamarole.html#generational-garbage-collection) Python (https://theconsensus.dev/project/python.html) 会退回到分代垃圾回收器来处理那些未通过引用计数释放且没有被任何存活对象引用的对象。 GC 模块 (https://docs.python.org/3/library/gc.html?from_theconsensus=1) 允许我们注册在 GC 开始和停止收集时执行的回调。我们会在收集开始时设置一个全局变量,在停止时取消设置。然后在我们的 `weakref.finalize` 回调中,我们可以打印该对象是否在 GC 收集期间被终结。如果没有,我们就知道是引用计数释放了该对象。 ``` import gc, weakref _in_gc = False def _track(phase, info): global _in_gc _in_gc = phase == "start" gc.callbacks.append(_track) def watch(obj, label): weakref.finalize(obj, lambda: print(label, "freed by", "GC" if _in_gc else "refcount")) class Obj(): pass o = Obj() watch(o, "o") del o # o freed by refcount p = Obj() p.me = p watch(p, "p") del p gc.collect() # p freed by GC ``` gc1.py 运行它。 ``` $ ./cpython3.14.4/python gc1.py o freed by refcount p freed by GC $ ./cpython3.14.5/python gc1.py o freed by refcount p freed by GC ``` ## 真的是通过引用计数终结的吗? (https://theconsensus.dev/p/2026/06/06/python-3-14-garbage-collection-rigamarole.html#was-it-actually-finalized-by-reference-counting) 对象既可以通过 GC 释放,也可以通过引用计数释放。在前面的一些例子中,我们使用了 `weakref.finalize` 来观察对象引用计数归零时的释放。我们怎么知道实际上是引用计数而不是 GC 进行的释放呢?我们可以禁用 GC,并观察终结器仍然发生。 ``` import gc, sys, weakref gc.disable() class Obj: pass a = Obj() weakref.finalize(a, print, "freeing "+hex(id(a))) print("a memory", hex(id(a))) # 在我的机器上运行时为 0x1005b4d70 # 0x1005b4d70 的 refcount 是 1: `a` print(sys.getrefcount(a)) # 2: `a` 本身和 sys.getrefcount 的参数 b = a print("b memory", hex(id(b))) # 与 `a memory` 相同,同一运行中为 0x1005b4d70 # 0x1005b4d70 的 refcount 是 2: `a` 和 `b` print(sys.getrefcount(a)) # 3: `a` 本身、`b` 和作为参数的对象 print(sys.getrefcount(b)) # 3: `b` 本身、`a` 和作为参数的对象 del b # 0x1005b4d70 的 refcount 是 1 print(sys.getrefcount(a)) # 2: `a` 本身和作为参数的对象 del a # 0x1005b4d70 的 refcount 是 0,被删除,观察到打印了 `freeing 0x1005b4d70` ``` refcount7.py 运行它。 ``` $ ./cpython3.14.4/python refcount7.py a memory 0xf6e64bf71160 2 b memory 0xf6e64bf71160 3 3 2 freeing 0xf6e64bf71160 $ ./cpython3.14.5/python refcount7.py a memory 0xec231a391160 2 b memory 0xec231a391160 3 3 2 freeing 0xec231a391160 ``` ## 暂停 (https://theconsensus.dev/p/2026/06/06/python-3-14-garbage-collection-rigamarole.html#pauses) 由于 GC 需要定期运行,而 Python (https://theconsensus.dev/project/python.html) 是单线程的,因此当 GC 运行时,程序执行似乎会暂停。最小化暂停是 GC 的目标之一。一种方法是将对象分为几代。根据 Python 文档: > GC 根据对象存活过的收集扫描次数将其分为三代。新对象被放在最年轻的一代(第 0 代)。如果对象在一次收集中存活下来,它就被移到下一代。由于第 2 代是最老的一代,该代中的对象在收集后仍留在那里。 > 为了决定何时运行,收集器会跟踪自上次收集以来的对象分配和释放次数。

相似文章

Python 3.15:未登上头条的功能

Hacker News Top

Python 3.15 引入了较小但值得注意的功能,包括优雅的 TaskGroup 取消和装饰器的上下文管理器改进,以及诸如延迟导入和 tachyon 分析器之类的主要功能。

观察 Go 的新垃圾回收器在堆中的移动

Lobsters Hottest

Go 1.26 将 Green Tea 设为默认垃圾回收器,提升了缓存友好性。本文通过 Go 和 C# 可视化堆分配,并讨论了非移动回收器和稀疏页面带来的挑战。

Unix GC 重制版

Hacker News Top

详解 Linux 内核 AF_UNIX 垃圾收集器的重写,包括背景、新的基于图的模型以及一个释放后使用漏洞。