CPython底层链表追踪孤立引用 设25%阈值化解老年代GC瓶颈

【python】Unreachable的对象咋回收的?generation又是啥? — 码农高天

码农高天

Python通过在容器创建时直接注册到底层双向链表来捕获代码层无法访问的循环引用,并要求最老代新增对象占比超过25%才触发全量扫描以避免程序性能衰退。

AI-assisted summary of 【python】Unreachable的对象咋回收的?generation又是啥? — original source. Check the original for full context.

Source published:

Key ideas

  • CPython 针对 unreachable 循环引用对象的回收,依赖于将所有容器对象在创建阶段即挂载至底层的 GC 专属双向链表中进行追踪。 (5:00)
  • 分代垃圾回收(Generation 0、1、2)采用梯级触发:Gen 0 计数值为新分配容器净增数,而 Gen 1 与 Gen 2 的计数本质上是上一代 GC 执行的次数。 (7:10)
  • 针对 Generation 2 中长生命周期对象堆积导致的扫描耗时膨胀,CPython 引入了 25% 新增未检测对象比例的触发前置限制。 (9:50)

Detailed notes

## Background 该视频由码农高天于 2024 年 8 月 17 日发布在 YouTube 平台。视频简介指出,本期内容主要介绍 Python 垃圾回收的基本机制,详细拆解哪些对象会被记录跟踪,以及分代回收(generation)的具体运作原理。

## TL;DR 高天从知识星球用户关于二叉树循环引用导致对象无法访问(unreachable)却仍能被回收的困惑切入,首先梳理了单纯引用计数在面对循环引用时的局限性。随后,他从 CPython 源码底层阐明了容器与非容器对象的差异:所有容器对象在创建并执行 PyObject_GC_Track 时都会被登记进全局双向链表,因此垃圾回收器能准确找到程序变量已无法触及的孤立环。最后,视频详解了 Generation 0、1、2 的分代回收假说、各代 count 与阈值的具体含义、链表合并逻辑,以及针对 Generation 2 长生命周期对象累积所引入的 25% 比例优化机制。

## Atmosphere & style 视频保持了码农高天一贯严谨而清晰的技术硬核风格,如同资深工程师在白板前层层剖析底层原理。讲解从实际代码问题自然过渡到 C 源码实现,条理分明且直击痛点。整体语调平实自信、循循善诱,既有抽丝剥茧解答疑惑的顿悟感,又充满对底层系统设计权衡的深入洞察。

## Bulletin - [05:00](https://www.youtube.com/watch?v=8cOJwmvcl90&t=300s) CPython 针对 unreachable 循环引用对象的回收,依赖于将所有容器对象在创建阶段即挂载至底层的 GC 专属双向链表中进行追踪。 - [07:10](https://www.youtube.com/watch?v=8cOJwmvcl90&t=430s) 分代垃圾回收(Generation 0、1、2)采用梯级触发:Gen 0 计数值为新分配容器净增数,而 Gen 1 与 Gen 2 的计数本质上是上一代 GC 执行的次数。 - [09:50](https://www.youtube.com/watch?v=8cOJwmvcl90&t=590s) 针对 Generation 2 中长生命周期对象堆积导致的扫描耗时膨胀,CPython 引入了 25% 新增未检测对象比例的触发前置限制。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=8cOJwmvcl90&t=0s) 引用计数归零是 CPython 最基础且直接的内存回收机制,该过程由底层自动处理,不依赖复杂的循环垃圾回收器。 - [00:00](https://www.youtube.com/watch?v=8cOJwmvcl90&t=0s) 当对象之间形成循环引用(reference cycle)且外部变量被删除时,引用计数无法归零,导致内存泄漏并形成 unreachable 孤立环。 - [05:00](https://www.youtube.com/watch?v=8cOJwmvcl90&t=300s) CPython 区分容器(container)与非容器对象:只有能引用其他对象的容器类型(如 list、dict、自定义实例)才会被 GC 追踪并可能产生循环引用。 - [05:00](https://www.youtube.com/watch?v=8cOJwmvcl90&t=300s) 非容器对象(如 int、string)直接通过 PyObject_Malloc 分配内存,不附加垃圾回收结构体开销。 - [05:00](https://www.youtube.com/watch?v=8cOJwmvcl90&t=300s) 容器对象在创建时会调用 PyObject_GC_New 分配带 GC 头部的内存,并通过 PyObject_GC_Track 将其直接插入垃圾回收器的全局双向链表。 - [05:00](https://www.youtube.com/watch?v=8cOJwmvcl90&t=300s) 即使 Python 代码层面删除了所有变量引用,垃圾回收器依然可以通过底层的专属双向链表遍历并找到这些不可达对象。 - [07:10](https://www.youtube.com/watch?v=8cOJwmvcl90&t=430s) 弱分代假说表明大多数对象生命周期极短,分代回收机制旨在避免对已知存活很久的对象进行频繁且无意义的重复全量扫描。 - [07:10](https://www.youtube.com/watch?v=8cOJwmvcl90&t=430s) 所有新建的容器对象初始统一进入 Generation 0,触发检查时遵循从老到新(Gen 2 -> Gen 1 -> Gen 0)的优先级顺序。 - [07:10](https://www.youtube.com/watch?v=8cOJwmvcl90&t=430s) Generation 0 的 count 统计新增容器净增量(默认阈值 700),而 Generation 1 和 2 的 count 统计的是上一代 GC 的触发次数(默认阈值均为 10)。 - [07:10](https://www.youtube.com/watch?v=8cOJwmvcl90&t=430s) 每当高代际触发垃圾回收时,所有比其年轻的代际链表都会合并到该代统一检查,扫描后存活对象沉淀至当前代中。 - [09:50](https://www.youtube.com/watch?v=8cOJwmvcl90&t=590s) 若不对老生代(Generation 2)加以限制,随着长寿命对象积累,全量扫描会使单位时间内 GC 耗时占比失控,导致程序运行变慢。 - [09:50](https://www.youtube.com/watch?v=8cOJwmvcl90&t=590s) CPython 为 Generation 2 增加了优化规则:仅当该代中未检测过的新对象占比超过 25% 时,才会真正执行全量垃圾回收。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=8cOJwmvcl90&t=0s) 循环引用与 Unreachable 对象的回收疑惑

高天首先分享了本期视频的背景:话题来源于他在知识星球上收到的一位朋友的提问。提问者给出了一个基于二叉树节点(`TreeNode`)的简化代码示例。`TreeNode` 是一个标准的二叉树节点,包含父节点(`parent`)以及左右子节点(`left`、`right`)。代码创建了两个节点 `node1` 和 `node2`,将 `node1.left` 指向 `node2`,同时把 `node2.parent` 指向 `node1`,随后使用 `del node1` 和 `del node2` 删除了这两个变量。在 Python 中,当这两个变量被删除后,程序在代码层面上就再也无法获取这两个节点了,这种无法被再次访问到的对象被称为“unreachable object”。

```mermaid flowchart LR accTitle: 循环引用对象结构 accDescr: 展示 node1 与 node2 互相引用并在变量被删除后形成孤立环的状态

subgraph Program["代码层面(已 del 变量)"] V1["变量 node1<br>(已删除)"] -.->|无法访问| N1 V2["变量 node2<br>(已删除)"] -.->|无法访问| N2 end

subgraph Heap["内存堆中的孤立环"] N1["TreeNode (node1)<br>引用计数: 1"] N2["TreeNode (node2)<br>引用计数: 1"] N1 -->|".left"| N2 N2 -->|".parent"| N1 end ```

此时如果手动执行 `gc.collect()`,垃圾回收器会返回并回收这两个对象。那位提问的朋友感到非常困惑:“明明这些 object 都已经 unreachable 了,你都拿不到它了,怎么垃圾回收机制还有能力回收它们呢?”提问者还提到官方文档中有一个叫做“generation”(代)的概念,也不理解究竟代表什么。

为了解答这两个疑惑,高天先讲解了 Python 最基础的内存管理机制——引用计数(reference counting)。在 Python 中,每当创建一个新对象,Python 都会在底层记录它有多少个引用。当运行完创建 `node1` 的语句时,对应的 `TreeNode` 对象上就有一个来自于变量 `node1` 的引用。在日常写 Python 时,开发者无需手动关心引用的增减,“因为引用的增加跟减少都是 CPython 内部帮你处理好了的”。当一个对象的引用计数归零时,就会被自动释放回收,这个过程完全不涉及后面复杂的循环垃圾回收机制,单纯是“一个简单的计数到零了,用不着了,free 掉”。

然而纯粹的引用计数存在致命的局限性: - 当创建 `node2` 并执行 `node2.parent = node1` 时,`node1` 的引用计数变为 2(变量 `node1` 和属性 `node2.parent`)。 - 若没有后续让 `node1.left = node2` 的代码,删除 `node2` 时其引用归零直接被回收;它被回收后,对 `node1` 的引用随之消失,`node1` 的计数也降为 0,两者都能自动回收,unreachable object 为 0。 - 一旦加上 `node1.left = node2`,事情就彻底变复杂了:`node1` 被 `node2.parent` 引用,`node2` 被 `node1.left` 引用,构成了“循环引用”(reference cycle)。 - 此时即使删除了变量名 `node1` 和 `node2`,两个对象在内存中依然互相引用,各自保留着 1 的引用计数,无法被引用计数机制释放,仍然白白占用内存。 - 循环引用在实际程序中可能非常庞大复杂,包含许多个对象。为此 Python 必须引入更复杂的垃圾回收机制(`gc.collect`),其核心工作就是识别并找出这些只有循环引用的孤立对象环并将其释放。

[05:00](https://www.youtube.com/watch?v=8cOJwmvcl90&t=300s) 容器与非容器:GC 是如何“找到”无法访问的对象的

为了回答“程序已经拿不到这个对象了,GC 怎么还能找得到它并进行回收”的问题,高天引入了“容器”(container)的概念。所谓容器,是指那些可以引用其他对象的对象,比如 `list`、`dict` 以及自定义的带有属性的实例对象。相反,像 `int`(在 CPython 内部为 `longobject`)或 `string` 这样的对象不能引用其他对象,因此不是容器,永远不会产生循环引用。

在 CPython 源代码层面,容器与非容器在分配内存和初始化时有着截然不同的路径: - 非容器对象(如整数)直接调用 `PyObject_Malloc` 来申请内存空间。 - 容器类型对象(如 `list`)在建立时则会调用 `PyObject_GC_New`。高天强调:“所有容器类型的 object 在建立的时候就要告诉它:你是一个容器,你要被 garbage collection 所意识到。” - `PyObject_GC_New` 随后调用 `_PyObject_GC_Alloc`,底层虽然同样使用 `PyObject_Malloc` 分配内存,但在内存头部带有垃圾回收专属信息,并通过 `_PyObject_GC_Link` 初始化其 GC 属性。 - 内存分配完成后,容器会执行 `PyObject_GC_Track`。这个“track”操作的本质,就是把该对象塞入垃圾回收器专门维护的全局双向链表中。

这就彻底解开了提问者的第一个疑惑:尽管在 Python 用户代码层面通过变量找不到该对象了,但在 C 语言底层,“每建立一个新的容器,我就把这个容器扔到了一个垃圾回收专门的链表里,这就是为什么尽管我们的 Python 程序已经找不到这个 object,垃圾回收还是能找得着,因为建立的时候就告诉了这个 garbage collection:这块有个 object,你注意点它。”每次触发垃圾回收时,GC 只需遍历这个链表中的容器,检测并清理那些只剩下内部循环引用的对象。

```mermaid flowchart TD accTitle: CPython 容器对象的内存分配与跟踪流程 accDescr: 展示容器对象通过 PyObject_GC_New 分配并在 track 时加入双向链表的过程

subgraph Allocation["内存分配阶段"] A["创建容器对象 (如 list)"] --> B["PyObject_GC_New"] B --> C["_PyObject_GC_Alloc"] C --> D["PyObject_Malloc 分配内存"] C --> E["_PyObject_GC_Link 初始化 GC 属性"] end

subgraph Tracking["GC 跟踪阶段"] E --> F["PyObject_GC_Track"] F --> G["加入 GC 双向链表<br>(Generation 0)"] end ```

[07:10](https://www.youtube.com/watch?v=8cOJwmvcl90&t=430s) 分代回收机制(Generation)与阈值合并逻辑

解答了追踪机制后,高天转入提问者的第二个问题:分代(generation)究竟是什么?为什么需要分代?

分代回收基于一个普遍的经验规律(弱分代假说):在实际编程中,绝大多数对象的生命周期都极短,往往用完立刻就能释放;只有少部分对象(如全局配置、长生命周期缓存)会存活很久。如果将所有容器都挂在同一个链表里进行全量扫描,检测循环引用的代价会极其高昂,高天指出:“随着程序的进展,这些生命周期很长的对象可能越来越多,就导致我们每一次垃圾回收都很慢,因为对一些我们已经检查了好几次、知道它已经存活了挺久的 object,我们还在不停地检查它。”而若单纯为了省时而降低垃圾回收频率,那些海量的短命对象又无法被及时释放。

为了平衡回收效率与开销,CPython 引入了 Generation 0、1、2 三个代际机制: - **初始分配**:所有新建的容器对象都会先被扔进 Generation 0。 - **触发顺序**:当需要检查时,程序会从老到新、从高到低检查是否达到阈值,即先检查 Generation 2,接着检查 1,最后检查 0。 - **计数值(count)的差异**:Generation 0 的 count 表示“新创建的容器对象数量”,新容器数量超过阈值就会触发 Generation 0 的 GC;但 Generation 1 和 2 的 count 并不代表该代内的对象数量,而是“代表上一个 generation 运行了多少次”,“也就是运行一次 generation 0 的垃圾回收,generation 1 的 count 只加 1”。 - **链表合并**:每次对某一代理论上做垃圾回收时,所有比当前代更年轻的代(younger generations)里面的对象都会被合并到当前代中统一进行检查。这意味着在完成一次 Generation 2 的检查后,Generation 0 和 1 的链表直接被清空,所有存活对象全部沉淀在 Generation 2 中。

| 代(Generation) | 默认阈值(Threshold) | count 的具体含义 | |:--- |:--- |:--- | | **Generation 0** | 700 | 自上次 GC 以来新分配的容器对象净增数量 | | **Generation 1** | 10 | Generation 0 垃圾回收触发并执行的次数 | | **Generation 2** | 10 | Generation 1 垃圾回收触发并执行的次数 |

- 在当前 CPython 版本中,Generation 0、1、2 的默认触发阈值分别是 700、10、10。 - 换算下来:每建立 700 个新容器触发一次 Gen 0 回收;每触发 10 次 Gen 0 回收触发一次 Gen 1 回收;每触发 10 次 Gen 1 回收(即经历了 100 次 Gen 0 回收后)才会触发一次 Gen 2 回收。

[09:50](https://www.youtube.com/watch?v=8cOJwmvcl90&t=590s) Final Generation 的长生命周期瓶颈与 25% 优化

在视频最后,高天深入分析了这套经典分代机制潜在的性能瓶颈。在创建容器对象速率恒定的情况下,每一代垃圾回收的触发频率基本是固定的。然而,这种机制在“最老的代”——Generation 2(Final Generation)中遇到了麻烦。

随着程序长时间运行,存活下来的长生命周期对象可能会不断累积,导致 Generation 2 中的对象数量越来越庞大。如果依旧严格按照固定频率对 Generation 2 进行全量扫描,由于长生命周期对象大概率不会被回收,“我们在每一次对 Generation 2 进行垃圾回收的时候,也是在不停地检查我们之前已经检查过的对象”,其直接后果就是“单位时间内做垃圾回收的时间占比越来越大,这样的结果就是我们程序越跑越慢,不是很理想”。

为了解决 Final Generation 的这一性能陷阱,CPython 引入了一项额外的优化策略: - 针对 Generation 2,即使达到触发计数的阈值,也不会盲目启动全量扫描。 - 垃圾回收器会评估新增未检测对象的比例,只有当 Generation 2 中尚未被检测过的对象数量占总对象数量的 25% 以上时,才会真正开启该代的全量垃圾回收。这一机制有效避免了在庞大且稳定的长寿对象集合上进行无意义的频繁扫描。

最后,高天总结了本期内容,表示希望这些原理解析能彻底解答那位星球同学的疑惑,并欢迎大家继续提问交流,提醒观众关注与一键三连。

## Notable quotes > [05:00](https://www.youtube.com/watch?v=8cOJwmvcl90&t=300s) 每建立一个新的容器,我就把这个容器扔到了一个垃圾回收专门的链表里,这就是为什么尽管我们的 Python 程序已经找不到这个 object,垃圾回收还是能找得着 — 高天

## Mentioned 高天, Python, CPython, 知识星球, TreeNode, gc.collect, PyObject_Malloc, PyObject_GC_New, _PyObject_GC_Alloc, _PyObject_GC_Link, PyObject_GC_Track

## Video description 这期视频我们介绍一下垃圾回收的基本机制,什么对象会被记录跟踪,以及generation是怎么回事。

[Source](https://www.youtube.com/watch?v=8cOJwmvcl90)

中文简报

CPython底层链表追踪孤立引用 设25%阈值化解老年代GC瓶颈 Python通过在容器创建时直接注册到底层双向链表来捕获代码层无法访问的循环引用,并要求最老代新增对象占比超过25%才触发全量扫描以避免程序性能衰退。 高天从知识星球用户关于二叉树循环引用导致对象无法访问(unreachable)却仍能被回收的困惑切入,首先梳理了单纯引用计数在面对循环引用时的局限性。随后,他从 CPython 源码底层阐明了容器与非容器对象的差异:所有容器对象在创建并执行 PyObject_GC_Track 时都会被登记进全局双向链表,因此垃圾回收器能准确找到程序变量已无法触及的孤立环。最后,视频详解了 Generation 0、1、2 的分代回收假说、各代 count 与阈值的具体含义、链表合并逻辑,以及针对 Generation 2 长生命周期对象累积所引入的 25% 比例优化机制。 CPython 针对 unreachable 循环引用对象的回收,依赖于将所有容器对象在创建阶段即挂载至底层的 GC 专属双向链表中进行追踪。 (5:00) 分代垃圾回收(Generation 0、1、2)采用梯级触发:Gen 0 计数值为新分配容器净增数,而 Gen 1 与 Gen 2 的计数本质上是上一代 GC 执行的次数。 (7:10) 针对 Generation 2 中长生命周期对象堆积导致的扫描耗时膨胀,CPython 引入了 25% 新增未检测对象比例的触发前置限制。 (9:50)

Reading summary

博主码农高天针对 Python 中无法访问对象的清理机制与分代回收(Generation)原理进行了解答,指出 CPython 通过专门的双向链表跟踪容器对象以识别循环引用,并结合三代回收与 25% 增量检查策略来平衡垃圾回收开销。 引用计数局限与循环引用的产生 (0:00) Python 最基础的内存管理机制是引用计数,CPython 会在底层自动跟踪对象的引用增减,当计数归零时直接释放内存。但这套机制无法处理循环引用。例如两个二叉树节点互相引用时,即使通过 `del` 删除了所有用户层面的变量名,这两个对象在内存中各自的引用计数仍为 1。它们在代码层面上已变为不可访问(unreachable),却无法被纯引用计数机制释放,必须依赖循环垃圾回收机制介入。 容器链表跟踪实现不可访问对象的检索 (5:00) 针对用户代码无法访问时垃圾回收器如何找到对象的问题,高天区分了容器与非容器对象。整数和字符串等非容器无法引用其他对象,永远不会产生循环引用,仅调用 `PyObject_Malloc` 分配内存;而列表、字典及自定义实例等属于容器,必须通过 `PyObject_GC_New` 分配内存,并在内存头部初始化 GC 属性。容器创建完成后会执行 `PyObject_GC_Track` 将自身加入垃圾回收器维护的全局双向链表中,垃圾回收机制正是通过遍历这些底层链表来定位并检测孤立的循环引用环。 三代回收机制与阈值合并逻辑 (7:10) 分代回收基于大部分对象生命周期极短的经验假设。为了避免反复全量扫描长生命周期对象,CPython 划分了 Generation 0、1、2 三个代际,新建容器均先归入 Generation 0。检查时遵循从高到低(Gen 2 到 Gen 0)的顺序,当代际触发回收时,所有更年轻代中的对象都会合并到当前代中统一扫描,存活对象随之沉淀到更老的代中。此外,Generation 0 的 count 表示新创建容器的净增数量,而 Generation 1 和 2 的 count 则分别记录上一代 GC 的执行次数;其默认触发阈值分别为 700、10、10,即经历 100 次 Gen 0 回收才会触发一次 Gen 2 回收。 Final Generation 的 25% 比例优化 (9:50) 随着程序长期运行,Generation 2(Final Generation)中会累积大量长生命周期对象。若仅凭固定的计数阈值盲目全量扫描,会导致垃圾回收在单位时间内的耗时占比不断扩大,拖慢程序运行。为此 CPython 增加了限制策略:即使 Generation 2 达到触发计数阈值,垃圾回收器也会评估新增未检测对象的比例,只有当 Generation 2 中未被检测过的对象数量占比超过 25% 时,才会正式开启全量回收,以此规避对稳定对象集合的无效扫描。 该机制通过在 C 语言层面挂载容器链表解决了不可达对象的定位难题,并依托代际合并与比例优化在回收彻底性与运行时性能之间取得了平衡。

Source quotes

[5:00] 高天: 每建立一个新的容器,我就把这个容器扔到了一个垃圾回收专门的链表里,这就是为什么尽管我们的 Python 程序已经找不到这个 object,垃圾回收还是能找得着

Original transcript

[0:00] 这期视频我们来简单聊一下拍档中垃圾回收也就是garbage collection的机制这个话题来自于我知识星球上一个朋友的提问我把他的代码简化了一下放到了这里这个tree node就是一个标准二叉数的节点啊有负节点还有左右子节点我们建立两个节点然后让node1的左是node2然后node2的负是node1接下来我们把两个变量都删除掉大家注意在删除掉这两个变量之后我们在python里就没有办法拿到这两个节点了这种我们已经没有办法再获取的对象叫做unreachable object我们可以通过手动的进行GC.collect进行垃圾回收大家可以看到我们回收了两个object也就是node1和node2对应的object那他的问题就是 明明这些object都已经unreachable了你都拿不到它了怎么垃圾回收机制还有能力回收它们呢他说他还看了文档说垃圾回收有generation这个概念这个概念是什么意思呢那这期视频呢 我们就尽量把这几个事情讲清楚那在讲这些事之前我们先介绍一下python最基本的垃圾回收机制也就是引用技术在 Python 中每当你建立了一个新的 objectPython都会帮你数着这个object有多少个引用比如说在运行完第9行之后我新建的这个tree node object上面就有一个引用就是node1这个variable当然在写python的时候呢你是不用考虑引用的事的因为引用的增加跟减少都是cpy到内部帮你处理好了的如果我们在建立object之后 直接把它delete掉这个object上面的引用就变成0了因为没有变量在指向他了当一个对象的引用变成0的时候它就会被自动回收这个自动回收是不涉及我们后面说的那个比较复杂的垃圾回收机制的它就是一个简单的技术到零了用不着了 free 掉但是 这个简单的引用技术有一个实际的问题我们看当我们建立了node2然后让node2的点parent等于node1的时候node1的这个对象就有两个引用了一个引用是node1还有一个引用是node2.parent那如果仅仅如此也是无所谓的因为Node2的TreeNode上面只有一个引用是Node2当我们删除node2的时候node二指相应的对象就被回收了因为他身上的引用是零了他被回收的时候他对node1的引用就没有了那由于我们提前delete了node1所以node1那个对象引用也变成0了所以这两个对象还是成功的被自动回收了我们看在没有第11行代码的情况下unreachable object是0但是这行代码就让事情变得更加复杂了因为这时候node1里面又包含了一个对node2的引用所以现在node1对应的object有两个引用一个是node1 一个是node2.parent而node2这个object也有两个引用一个是node2 一个是node1.left这种情况我们就叫做循环引用那我们现在是最简单的循环引用这个环里面只有两个object在实际程序里这个环可能很大有很多个object那循环引用就导致了下面这个问题即便我们把node1跟node2都删掉了这两个对象依然是身上有引用的他们彼此引用着对方所以尽管我们在程序里已经没有办法获取这两个对象了但是由于这两个对象身上都有引用存在所以他们还站着内存他们还在那这个时候就要引入一个相对更加复杂的垃圾回收机制也就是我们这里写的GC.Clent那这个垃圾回收核心就是针对这些循环引用所进行的垃圾回收他就是去判断现在存在的对象是不是所有的引用都是这种循环引用如果是的话他就可以被free掉了那检测循环引用的方法相对来说是比较复杂的我们这期视频先不讲我们先去回答一下知识星球这个朋友的提问这两个对象你都拿不到了你是怎么判断循环引用以及把他们回收的呢那在聊天事之前呢我们就讲一个小概念啊叫做container也就是容器所谓容器就是可以引用其他对象的对象比如说 list,比如说 dictionary,比如说我们这里自定义的object有attributed object,这些东西都叫container那什么东西不是container呢?比如说int,比如说string这些对象他不引用其他的对象他就不是container那这些对象就不存在循环引用的问题那如果你去读一下cpy的原代码 你就会发现容器和非容器在建立的时候它是不太一样的比如说我们这里的long object也就是integer它在建立的时候是使用的py object malloc来获取的内存而容器类型的object比如说list它使用的是PyObject GC newGC就代表Garbage Collection也就是说 [5:00] 所有容器类型的object在建立的时候就要告诉他你是一个容器你要被garbage collection所意识到那这个PyObject GC new最后调用的是这个GC alloc而GC alloc最终调用的还是PyObjectMalloc他拿内存的方式和非容器的object是一样的只不过这个size略有不同我们可以看到在这个GC alloc里面有一个Py object GC link这个函数就是用来初始化它垃圾回收相关的属性的我们看list object在拿完内存之后要做一个py object gc track而这个track就是把这个object放到垃圾回收的一个双项链表里也就是说每建立一个新的容器我就把这个容器扔到了一个垃圾回收专门的链表里这就是为什么尽管我们的python程序已经找不到这个object,垃圾回收还是能找得着因为建立的时候就告诉了这个garbage collection这块有个object 你注意点它那每一次触发垃圾回收的时候垃圾回收都会从所有的这些容器里面去找看看哪个容器只剩下循环引用了然后把这个容器释放这我们就回答第一个问题啊垃圾回收是怎么拿到这个object的第二个问题是有关 Generation他说我看python的官方文档里面这块有一个东西叫做generation什么叫generation呢它存在的意义是什么我们看在刚才track的代码里面就有generation0对吧我们是把object放到generation0里面的那我们先来讲为什么要generation因为人们发现在实际编程的过程中有大量的object生命周期非常短用完就可以扔了而有少部分的变量生命周期非常长 一时半会儿且扔不掉如果我们把所有的container都放到一个链表里一直找这个链表我们每找一次那些生命周期很长的对象都会被检查一遍而判断循环引用是一个比较耗时的事情随着程序的进展这些生命周期很长的对象可能越来越多就导致我们每一次垃圾回收都很慢因为对一些我们已经检查了好几次知道它已经存活了挺久的object我们还在不停的检查它那如果只是简单的把垃圾回收的频率调低那些大量出现的短生命周期的object又不能及时的得到回收正是为了去解决这个问题就出现了generation这个generation的理念就是当你新建一个容器object之后我永远把它先放到generation0里面当Generation 0里面的对象数量超过一定预值的时候开启 garbage collection在做garbage collection的时候,他会去选择做哪个generation的collection这里程序会从高到低去检测每个generation是不是应该做垃圾回收了也就是先检查Generation 2 然后检查1 最后检查0检查的条件就是这个generation的count是不是大于这个预值了这里注意一下 Generation012的count意义是不一样的我们前面介绍了generation 0的count就是有多少个新容器也就是说新容器的数量超过率值就会触发垃圾回收而Gen1跟2的count代表上一个generation运行了多少次也就是运行一次generation 0的垃圾回收,generation 1的count只加1这个count不代表generation1里面对象的数量每一次在进行垃圾回收检查的时候都会把所有比当前generation年轻的generation里面的所有对象给合并到当前generation里然后进行一次检查也就是说在进行一次generation2的检查之后generation1跟generation0里面都没有东西了没有被回收掉的对象全都放到了Generation2里面那在当前版本的Cpython中Generation 012的预值分别是700 10和10也就是每建立700个容器触发一次generation0的垃圾回收每10次generation0的垃圾回收触发一次generation1的垃圾回收每10次generation1的垃圾回收触发一次generation2的垃圾回收我们注意到如果按这个方式进行垃圾回收在我们产生容器变量速度基本不变的情况下每一级别的垃圾回收它的频率基本是一致的但是这会带来一个问题这个问题出在我们的final generation也就是generation2里面我们可以想象随着程序的运行我们的长生命周期对象 [10:00] 有可能越来越多的也就是说Generation 2里面的对象的数量可能是越来越多的如果我们保持对Generation 2的垃圾回收检测的频率不变而Generation 2里面的对象数量又越来越多就可能会导致我们单位时间内做垃圾回收的时间占比越来越大这样的结果就是我们程序越跑越慢 对吧 不是很理想即便Generation2的对象没有越来越多,我们在每一次对Generation2进行垃圾回收的时候,也是在不停的检查我们之前已经检查过的对象。这些对象大概率是不会被垃圾回收的这个结果依然不是很理想所以cpython还引入了另外一个优化机制对于final generation也就是generation 2只有在没有检测过的对象数量占总对象数量的25%以上的时候才开启这个generation的垃圾回收以上就是我们对Cpython垃圾回收机制的一个简单介绍希望这个介绍可以解答这位同学的疑惑啊那加入我知识星球的朋友如果还有其他问题也欢迎在知识星球里面向我提问我会尽可能的解答大家的问题好了 这期视频就到这里 请关注 别忘了意见三零哦