广告
您当前的位置: 首页 >  技术 >  Python

Python 中的垃圾回收 (Garbage Collection) 机制与 gc 模块底层核心原理解析

作者:CoderWang 时间:2026-06-23 阅读数:4人阅读

在高级编程语言中,内存管理通常由运行时系统自动代劳。Python 作为一门动态、强类型的解释型语言,提供了自动化的垃圾回收(Garbage Collection, 简称 GC)机制,极大地降低了内存泄漏的风险。然而,在面对高并发、海量数据的业务场景时,默认的垃圾回收策略可能会带来显著的性能波动。例如著名的“Stop the World”停顿问题。

要写出高性能、低内存占用的 Python 代码,必须深入理解其背后的垃圾回收机制。Python(特别是官方的 CPython 实现)的垃圾回收是由 “引用计数为主,分代回收和标记-清除为辅” 的混合策略组成的。本文将深入 CPython 源码逻辑,全面拆解 Python 垃圾回收的核心原理。


一、 核心支柱:引用计数 (Reference Counting)

引用计数是 CPython 内存管理的第一道防线。它的原理非常直观:每一个 Python 对象在底层结构体 PyObject 中都包含一个引用计数器成员(ob_refcnt)。

1. 引用计数的工作机制

  • 当一个对象被创建(例如 a = 10)、被引用(例如 b = a)、作为参数传入函数、或者被放入列表等容器中时,它的 ob_refcnt 会加 1。
  • 当对象的引用被显式销毁(例如 del a)、超出作用域(例如函数执行完毕)、或者其所在的容器被销毁时,它的 ob_refcnt 会减 1。
  • 一旦一个对象的引用计数归 0,说明该对象已不再能被任何代码访问,Python 运行时会立即释放它所占用的内存空间。
import sys

# 观察引用计数的变化
a = [1, 2, 3]
print(sys.getrefcount(a))  # 输出 2 (一个是变量 a,另一个是传入 getrefcount() 的临时参数引用)

b = a
print(sys.getrefcount(a))  # 输出 3 (增加变量 b 引用)

del b
print(sys.getrefcount(a))  # 输出 2

2. 引用计数的优缺点

  • 优点
    • 实时性高:一旦对象不再需要,内存会立刻被释放,没有延迟。
    • 停顿时间短:内存释放分散在代码执行的每个瞬间,不会导致程序出现大范围卡顿。
  • 致命缺陷:无法解决循环引用 (Cyclic References) 问题。

二、 引用计数的死穴:循环引用

所谓的循环引用,是指两个或多个对象之间互相引用,形成了一个封闭的环状结构。即使外界没有任何变量指向它们,它们的引用计数依然不为 0。

循环引用示例:

class Node:
    def __init__(self, name):
        self.name = name
        self.next = None

# 创建两个互相指向的节点
node_a = Node("A")
node_b = Node("B")
node_a.next = node_b
node_b.next = node_a

# 此时 node_a 引用计数为 2 (变量 node_a + node_b.next)
# node_b 引用计数为 2 (变量 node_b + node_a.next)

# 显式解除外部引用
del node_a
del node_b

当执行 del 之后,外部的局部变量指针已经不存在了。然而,Node("A")Node("B") 互相之间还在指向对方,它们的 ob_refcnt 依然为 1。由于它们不为 0,引用计数器将永远无法将其销毁,从而在内存中形成“僵尸对象”,造成持久的内存泄漏。

为了解决这个问题,CPython 引入了专门针对容器对象(如列表、元组、字典、用户自定义类等可能产生循环引用的类型)的垃圾回收器——分代回收(Generational GC)


三、 拯救循环引用:分代回收与标记-清除

分代回收的底层基于两个核心算法思想:弱代假说 (Weak Generational Hypothesis) 以及 标记-清除 (Mark-Sweep) 算法。

1. 弱代假说

弱代假说是垃圾回收领域的经验法则:“新生的对象更容易夭折,而存活时间越长的对象,其生命周期也越长”。 为了提高效率,Python 将所有被 GC 追踪的容器对象划分为 3 个不同的“世代(Generations)”: * 0 代 (Generation 0):存放最新创建的对象。它的回收最为频繁。 * 1 代 (Generation 1):在 0 代回收中存活下来的对象,会被晋升到 1 代。 * 2 代 (Generation 2):在 1 代回收中依然存活的对象,晋升到 2 代。它的回收周期最长,存放的是长期存活的对象(如全局变量、模块等)。

2. 触发阈值 (Thresholds)

CPython 内部维护了一个计数器数组,用于记录各代的增减情况。当某个世代的触发指标达到预设阈值时,就会触发对应世代及以下世代的垃圾回收。 我们可以使用 gc.get_threshold() 查看这些阈值:

import gc
print(gc.get_threshold())  # 默认输出: (700, 10, 10)
  • 700:表示当“0代中新创建的容器对象”减去“被释放的容器对象”的净增值超过 700 时,触发 0 代 GC。
  • 第一个 10:表示每发生 10 次 0 代回收,就会触发一次 1 代和 0 代的联合回收。
  • 第二个 10:表示每发生 10 次 1 代回收,就会触发一次 2 代以及全世代的完整垃圾回收(Full GC)。

3. 标记-清除 (Mark-Sweep) 的底层逻辑

在触发垃圾回收时,Python 如何找出那部分孤立的循环引用呢? 1. 对象双向链表:所有可容纳其他引用的容器对象在创建时,都会被挂载到垃圾回收器追踪的双向链表上。 2. 拷贝引用计数(gc_refs:在 GC 开始时,垃圾回收器会将追踪链表上每个对象的 ob_refcnt 复制一份到 gc_refs 字段中。 3. 试探性减 1(Trial Deletion):GC 遍历所有追踪对象。如果对象 A 引用了对象 B,那么就将对象 B 的临时引用计数 gc_refs 减 1。遍历结束后,所有的外部引用依然保留,但内部的循环引用已经被冲抵掉了。 4. 标记可达与不可达: * 此时如果一个对象的 gc_refs 大于 0,说明外界(非循环引用)依然持有该对象的引用。该对象被标记为“可达”(REACHABLE)。 * 当一个对象被标记为可达后,它所引用的所有关联对象也会递归地被标记为“可达”。 * 最终,那些 gc_refs 为 0 且无法从任何活动对象触达的对象,即为“不可达”(UNREACHABLE),它们就是需要被清除的垃圾。 5. 清除(Sweep):将标记为不可达的对象移出链表并进行物理销毁。


四、 实战:使用 gc 模块调优与排查

Python 提供了官方的 gc 标准库,允许开发者手动控制垃圾回收器的行为。

1. 排查循环引用

我们可以借助 gc.get_referrers() 查询哪些对象引用了某个对象,从而找出导致对象无法被释放的循环引用源头。

import gc

class Target:
    pass

a = Target()
b = Target()
a.partner = b
b.partner = a

# 查看引用了对象 a 的所有对象
referrers = gc.get_referrers(a)
print("Referrers of 'a':")
for r in referrers:
    print(type(r), "-->", r)

2. 手动控制垃圾回收

在某些对延迟要求极高的实时系统(如游戏服务器、量化交易系统)中,我们可以主动关闭自动 GC,并在业务空闲期(例如处理完一笔交易后)手动触发垃圾回收,以此规避随机停顿:

import gc

# 禁用自动垃圾回收
gc.disable()

# 执行一些极其耗费 CPU 的核心循环(消除 GC 扫描带来的性能开销)
# ...

# 业务闲时:手动触发一次完全垃圾回收
gc.collect()

# 重新启用自动垃圾回收
gc.enable()

五、 高级避坑指南与最佳实践

1. 谨慎定义 __del__ 析构方法

在 Python 3.4 之前,如果发生循环引用的两个对象均定义了 __del__ 析构函数,Python 会因为不知道应该先调用哪个对象的析构函数而无法将其销毁。这些对象会被移入 gc.garbage(未妥善处理的垃圾)中并永久驻留内存。 * 现代 Python (>=3.4):PEP 442 解决了此问题,GC 可以安全地销毁带有 __del__ 的循环引用对象,但频繁触发 __del__ 依然会增加对象生命周期管理的复杂性。尽量改用 contextlib 上下文管理器来管理资源释放,少用 __del__

2. 利用 weakref (弱引用) 斩断循环环路

如果你必须在设计模式中让两个对象相互关联,可以使用 weakref(弱引用)模块。弱引用不会增加对象的引用计数。当主对象被销毁时,弱引用会自动失效。

import weakref

class Master:
    def __init__(self):
        self.worker = None

class Worker:
    def __init__(self, master):
        # 使用弱引用指向 master,防止构成循环引用
        self.master_ref = weakref.ref(master)

    @property
    def master(self):
        return self.master_ref()  # 获取弱引用关联的对象,如果已释放则返回 None

m = Master()
w = Worker(m)
m.worker = w  # master 指向 worker (强引用)
# worker 指向 master 的是弱引用,此时没有构成循环强引用,内存可以安全释放!

3. 避免大列表/大字典频繁生成造成 0 代阈值抖动

如果程序在短时间内创建了海量的小字典、小元组,会使 Python 的 0代对象计数器瞬间爆表,导致垃圾回收器频繁被唤醒。 * 优化方法:使用生成器(Generators)流式处理数据,或者在大规模循环中,通过 gc.set_threshold() 适当调大第一代的阈值(例如将 700 改为 2000),从而减少扫描的频次,大幅缩短程序的整体运行耗时。

总结

引用计数为 Python 带来了极其平稳、敏捷的内存即时释放体验,而分代回收则为循环引用的解套提供了最后的安全防线。了解这一底层运行机制,不仅能帮助我们彻底规避由于不小心的“相互持有”带来的内存泄漏隐患,更能让老练的工程师在处理核心计算节点时,通过调整阈值、结合弱引用,将 Python 的运行效率提升到新的高度。

本站所有文章、数据、图片均来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。

评论交流 (0)

正在加载评论...
头像

CoderWang

当你还撑不起你的梦想时,就要去奋斗。如果缘分安排我们相遇,请不要让她擦肩和过。我们一起奋斗!

微信