Python 的内存管理机制是程序高效运行的基石。要写出高性能、不内存泄漏的代码,必须深入理解其背后的垃圾回收(GC)逻辑。Python 的垃圾回收采用“引用计数为主,分代回收为辅”的策略,同时辅以“标记-清除”机制解决循环引用问题。
以下将详细拆解这套机制的工作原理与实操方法。
1. 掌握引用计数机制
引用计数是 Python 最底层的内存管理手段,其核心思想简单直接:记录对象被引用的次数。
理解对象结构:每一个 Python 对象在内存中都有一个头部信息 PyObject,其中包含一个 ob_refcnt 变量,这就是该对象的引用计数值。
观察引用计数的变化规律。当 ob_refcnt 降为 0 时,意味着对象不再被使用,内存会被立即回收。
熟悉导致引用计数增加的 4 种情况:
- 创建对象:例如
a = 23。 - 赋值引用:例如
b = a,此时a和b指向同一个对象。 - 传入函数:将对象作为参数传递给函数,例如
func(a)。 - 加入容器:将对象存储在列表或字典中,例如
list1 = [a, a]。
熟悉导致引用计数减少的 4 种情况:
- 销毁别名:使用
del a显式删除引用。 - 赋值新对象:例如
a = 24,旧对象23的引用计数减 1。 - 离开作用域:函数执行完毕,局部变量自动销毁,引用计数减 1。
- 移出容器:对象所在的容器被销毁,或者从容器中删除了该对象。
使用 sys.getrefcount() 查看对象引用计数。
运行以下代码观察引用计数的变化:
import sys
a = 11
print('初始:', sys.getrefcount(11) - 1) # 减1是因为getrefcount本身也会引用一次参数
b = a
print('赋值后:', sys.getrefcount(11) - 1)
def func(c):
print('函数内:', sys.getrefcount(c) - 1)
func(a)
del b
print('删除b后:', sys.getrefcount(11) - 1)
警惕引用计数的致命弱点:循环引用。如果两个对象相互引用,即使外部没有其他变量引用它们,它们的引用计数也永远不会降为 0,从而导致内存泄漏。
验证循环引用问题:
# 创建两个列表
list1 = []
list2 = []
# 相互引用
list1.append(list2)
list2.append(list1)
# 删除外部引用
del list1
del list2
# 此时内存中这两个对象依然互相引用,引用计数不为0,无法被回收
2. 理解标记-清除机制
为了解决循环引用导致的内存泄漏,Python 引入了“标记-清除”机制作为辅助手段。该机制主要针对容器对象(如 list、dict、class 等)。
识别根节点:垃圾回收器会从一组“根对象”出发,这些根对象包括全局变量、调用栈等。这些对象被认为是绝对活跃的。
执行标记阶段:从根对象出发,沿着引用关系遍历所有对象。所有能被访问到的对象都被标记为“存活”。
执行清除阶段:遍历完所有对象后,那些没有被标记为“存活”的对象,就是不可达的垃圾对象,回收器会清除它们。
理解处理流程:
(待回收)"] B --> C["从根对象(Root)出发遍历"] C --> D["将可达对象标记为黑色
(存活)"] D --> E["检查灰色对象引用"] E --> F{遍历完成?} F -- 否 --> E F -- 是 --> G["回收所有白色对象"] G --> H["结束 GC"]
通过这种方式,即使 list1 和 list2 相互引用,只要它们无法从根对象被追踪到,就会被正确识别并回收。
3. 优化分代回收机制
如果每次内存分配都检查整个内存的引用关系,程序运行效率会极低。为了解决这个问题,Python 采用了分代回收策略。这是一种“以空间换时间”的优化方案。
掌握核心假设:对象存活时间越长,越不可能是垃圾。新创建的对象往往很快就会变成垃圾(比如临时变量),而老对象通常会长期存在。
区分三代对象:
- 第 0 代(年轻代):新创建的对象都放在这里。垃圾回收频率最高。
- 第 1 代(中年代):如果在一次第 0 代垃圾回收中存活下来,对象会被移到第 1 代。
- 第 2 代(老年代):如果在第 1 代垃圾回收中依然存活,对象会被移到第 2 代。这里的垃圾回收频率最低。
查看默认阈值(不同版本可能略有差异):
| 代数 | 阈值含义 | 默认阈值示例 |
|---|---|---|
| 0 | 触发0代回收的分配次数减去释放次数 | 700 |
| 1 | 触发1代回收所需的0代回收次数 | 10 |
| 2 | 触发2代回收所需的1代回收次数 | 10 |
理解触发流程:
- 分配内存时,计数器更新。
- 检查计数器是否超过阈值。例如,当第 0 代分配的内存数量达到 700 个单位,Python 就会自动触发一次第 0 代的垃圾回收。
- 执行回收:
- 如果触发的是第 0 代回收,会清理第 0、1、2 代。
- 如果触发的是第 1 代回收,会清理第 1、2 代。
- 如果触发的是第 2 代回收,仅清理第 2 代。
- 晋升存活对象:在第 0 代回收中存活下来的对象,会被移动到第 1 代,依此类推。
4. 实用代码优化建议
基于上述机制,编写 Python 代码时可以采取以下措施提升性能。
避免不必要的循环引用。如果在代码结构中必须使用循环引用(例如树状结构或双向链表),确保在对象销毁时手动 打破 引用环(例如将引用设为 None)。
使用 gc 模块监控和调试。Python 提供了 gc 模块允许开发者手动干预垃圾回收。
收集垃圾对象:调用 gc.collect() 可以立即执行一次完整的垃圾回收,这在处理大规模数据后释放内存时非常有用。
import gc
# 手动执行一次垃圾回收
collected = gc.collect()
print(f"回收了 {collected} 个对象")
调整回收阈值:如果程序中产生了大量短期对象,可能会频繁触发 GC,影响性能。可以通过 gc.set_threshold(threshold0[, threshold1[, threshold2]]) 调整阈值,减少 GC 触发频率。
管理长期对象:对于那些需要长期使用的变量(例如配置信息、缓存),尽量让它们尽快进入老年代,避免它们在年轻代中频繁被扫描。
检查垃圾回收状态:使用 gc.get_stats() 查看各代的回收统计信息,辅助分析内存瓶颈。

暂无评论,快来抢沙发吧!