Skip to content

1. 内存管理与垃圾回收基础

1.1 什么是内存管理和垃圾回收

程序运行时需要不断申请内存存储数据,当数据不再使用时就要释放内存给其他数据用。内存管理就是管理这个申请-使用-释放的过程。

垃圾回收(Garbage Collection, GC)会自动识别并释放不再使用的内存空间。不再使用的内存叫做"垃圾"。有了垃圾回收机制,程序员就不用手动管理内存分配和释放,避免了内存泄漏和悬挂指针等问题。

1.2 Python 垃圾回收的三大机制

Python 用三种机制来做垃圾回收:

  1. 引用计数(Reference Counting):主要机制,通过跟踪对象的引用数量来决定是否回收
  2. 标记-清除(Mark and Sweep):解决循环引用问题
  3. 分代回收(Generational Collection):优化垃圾回收效率

三种机制各有优缺点,配合使用保证了安全高效的垃圾回收。

2. 引用计数机制

2.1 引用计数原理

引用计数是 Python 最核心的垃圾回收机制。每个 Python 对象都有一个引用计数器,记录有多少个引用指向这个对象。

引用计数增加的情况

  • 变量赋值a = obj 创建新的引用
  • 函数参数传递:将对象作为参数传递给函数(离开作用范围就会-1)
  • 容器操作:将对象放入列表、字典等容器中
  • 属性赋值obj.attr = other_obj 创建属性引用
python
import sys
a = [1, 2, 3]                    # 引用计数 = 1
b = a                            # 引用计数 = 2(变量赋值)
container = {'list': a}          # 引用计数 = 3(放入容器)
print(sys.getrefcount(a) - 1)    # 输出: 3(getrefcount会临时+1)

引用计数减少的情况

  • 变量重新赋值a = new_obj 原对象引用计数减 1
  • 变量离开作用域:函数结束时局部变量被销毁
  • 容器删除:从列表、字典中删除对象
  • 显式删除:使用del语句删除变量
python
del b                            # 引用计数 = 2
del container['list']            # 引用计数 = 1
# 当最后一个引用被删除时,对象被立即回收

2.2 引用计数的优缺点

优点:

  • 实现简单,容易理解
  • 垃圾回收是确定性的,一旦对象不再被引用,就会立即被回收
  • 分散回收操作的时间开销,避免了长时间的 GC 暂停

缺点:

  • 无法解决循环引用问题
  • 维护引用计数需要额外的内存和计算开销
  • 每次引用操作都需要更新计数器,影响性能

2.3 示例:引用计数示例

python
import sys

def reference_count_demo():
    """演示引用计数的完整生命周期"""
    # 1. 创建对象
    data = [1, 2, 3, 4, 5]
    print(f"创建后引用计数: {sys.getrefcount(data) - 1}")  # 1

    # 2. 变量赋值增加引用计数
    data_ref = data
    print(f"变量赋值后: {sys.getrefcount(data) - 1}")      # 2

    # 3. 放入容器增加引用计数
    container = {'list': data}
    print(f"放入字典后: {sys.getrefcount(data) - 1}")      # 3

    # 4. 函数调用临时增加引用计数
    def process(lst):
        print(f"函数内部: {sys.getrefcount(lst) - 1}")     # 4
    process(data)
    print(f"函数调用后: {sys.getrefcount(data) - 1}")      # 3

    # 5. 删除引用减少引用计数
    del data_ref
    print(f"删除data_ref后: {sys.getrefcount(data) - 1}")  # 2

    del container['list']
    print(f"从字典删除后: {sys.getrefcount(data) - 1}")    # 1

reference_count_demo()

3. 标记-清除机制——解决循环引用

3.1 循环引用问题

两个或多个对象互相引用,但没有外部引用指向它们时,就形成了循环引用。纯引用计数无法处理这种情况——即使对象不再被使用,引用计数也不会降为 0,造成内存泄漏。

例如:

python
def create_cycle():
    # 创建两个互相引用的对象
    lst_a = []
    lst_b = [lst_a]  # lst_b引用lst_a
    lst_a.append(lst_b)  # lst_a引用lst_b

    # 此时即使没有外部引用,lst_a和lst_b的引用计数都是1
    # 函数结束后,这些对象不会被回收,形成内存泄漏

create_cycle()

3.2 标记-清除机制的工作原理

为了解决循环引用问题,Python 使用标记-清除(Mark and Sweep)算法。这个算法主要基于三色标记模型,将对象分为三类:

  • 白色:未被访问的对象(可能是垃圾)
  • 黑色:已被访问,且引用的所有对象也都已访问(安全对象)
  • 灰色:已被访问,但引用的对象还未完成访问(中间状态)

标记阶段(Mark Phase)

GC 会把所有对象放入双向链表中。从根对象(全局变量、当前栈帧中的局部变量、寄存器等)开始遍历:

  1. 将所有根对象标记为灰色
  2. 从灰色对象集合中取出一个对象,将其引用的所有对象标记为灰色
  3. 将该对象标记为黑色
  4. 重复步骤 2-3,直到灰色对象集合为空。

清除阶段(Sweep Phase)

遍历结束后:

  • 黑色对象:是可达的,保留。
  • 白色对象:是不可达的,回收。
python
# 循环引用检测原理:不看引用计数,只看对象是否可达
a, b = {}, {}
a['ref'], b['ref'] = b, a  # 互相引用
del a, b  # 删除外部引用后,标记阶段无法到达它们(始终保持白色),会被回收

算法的时间复杂度

  • 标记阶段:O(N),N 是可达对象数量
  • 清除阶段:O(M),M 是所有对象数量
  • 缺点:GC 执行期间需要暂停程序(Stop The World),虽然 Python 做了优化,但处理大量对象时仍可能有性能影响。

3.3 gc 模块详解

gc 模块是 Python 提供的垃圾回收控制接口。不仅可以控制回收行为,还能用于调试内存泄漏。

3.3.1 常用控制功能

最基础的开关和手动回收功能。

python
import gc

# 查看和控制 GC 状态
print(f"GC是否启用: {gc.isenabled()}")  # True
gc.disable()  # 禁用自动回收(常用于性能调优)
gc.enable()   # 重新启用

# 手动触发回收
# 注意:这会触发完整的 FULL GC,开销较大
collected_count = gc.collect()
print(f"本次手动回收了 {collected_count} 个对象")

# 查看回收阈值 (700, 10, 10)
# 当分配对象数 - 释放对象数 > 700 时,触发0代回收
print(f"当前阈值: {gc.get_threshold()}")

3.3.2 调试与诊断功能 (Debug Flags)

gc 模块提供了强大的调试标记,可以打印回收过程或保存无法回收的对象。

python
import gc

# 设置调试标记
# DEBUG_STATS: 打印回收统计信息
# DEBUG_LEAK: 包含 DEBUG_COLLECTABLE | DEBUG_UNCOLLECTABLE | DEBUG_SAVEALL
gc.set_debug(gc.DEBUG_LEAK)

# 调试模式下,回收的对象不会被释放,而是保存在 gc.garbage 中
gc.collect()

# 分析不可回收的对象
print(f"发现 {len(gc.garbage)} 个不可回收/泄露的对象")
for obj in gc.garbage[:3]:
    print(f"泄露对象类型: {type(obj)}")

# 清理现场:手动清除 garbage 并恢复调试标记
gc.garbage.clear()
gc.set_debug(0)

3.3.3 对象引用关联分析

想要知道某个对象被谁引用了?或者它引用了谁?

python
import gc

a = [1, 2, 3]
b = {'ref_to_a': a}

# 1. 查看 a 被谁引用了 (get_referrers)
# 注意:结果通常包含当前栈帧等系统对象
refs = gc.get_referrers(a)
print(f"对象 a 被 {len(refs)} 个对象引用")

# 2. 查看 b 引用了谁 (get_referents)
targets = gc.get_referents(b)
print(f"对象 b 引用了 {len(targets)} 个对象 (key和value)")

3.4 示例:使用 gc 模块可视化回收过程

这个示例演示了完整的循环引用创建、检测、回收全过程。

python
import gc


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

    def __repr__(self):
        return f"Node({self.name})"

    def __del__(self):
        print(f"  [析构] {self.name} 正在被销毁...")


def demonstrate_gc_workflow():
    print("=== GC 模块深度演示 ===")

    # 1. 开启调试模式,观察回收过程
    # DEBUG_STATS 会打印 GC 运行的详细日志
    gc.set_debug(gc.DEBUG_STATS)

    def create_cycle_garbage():
        print("\n1. 创建循环引用对象...")
        n1 = Node("A")
        n2 = Node("B")
        n1.ref = n2
        n2.ref = n1
        print("   对象创建完毕,即将离开作用域")
        # 函数结束,n1, n2 引用计数减 1,但因循环引用不为 0

    # 禁用自动回收,手动控制节奏
    gc.disable()

    create_cycle_garbage()

    print(f"\n2. 目前 garbage 列表大小: {len(gc.garbage)}")
    print("   (对象虽然不可达,但还未被 GC 扫描发现)")

    print("\n3. 手动触发 GC...")
    # generation=2 表示执行 Full GC
    collected = gc.collect(generation=2)

    print(f"\n4. GC 结束,回收了 {collected} 个对象")
    print(f"   garbage 列表 (调试模式下保留): {gc.garbage}")

    # 恢复环境
    gc.set_debug(0)
    gc.garbage.clear()
    gc.enable()


demonstrate_gc_workflow()

4. 分代回收机制

4.1 分代回收的基本概念

分代回收基于一个观察:新对象通常很快就死掉,老对象往往继续存活。Python 把对象按存活时间分为三代:

  • 第 0 代(Generation 0):新创建的对象,死亡率最高
  • 第 1 代(Generation 1):从第 0 代幸存下来的对象
  • 第 2 代(Generation 2):从第 1 代幸存下来的对象,存活时间最长

参与分代回收的对象

只有可能产生循环引用的容器对象才参与分代回收:

python
# 参与分代回收的对象
containers = [[], {}, set(), object()]  # list, dict, set, 自定义类实例

# 不参与分代回收的对象
non_containers = [42, "hello", None]    # int, str, None等基础类型

这样设计可以优先处理最可能成为垃圾的新对象,提高回收效率。

4.2 分代回收的工作原理

分代回收通过阈值系统对象晋升机制来优化回收效率:

回收触发机制

Python 使用三个阈值控制各代的回收(默认值:700, 10, 10):

python
import gc

def show_gc_mechanism():
    thresholds = gc.get_threshold()
    counts = gc.get_count()

    print(f"回收阈值: {thresholds}")
    print(f"当前各代对象数量: {counts}")

    print("\n触发条件:")
    print(f"第0代回收: 对象数达到 {thresholds[0]} 时")
    print(f"第1代回收: 第0代回收 {thresholds[1]} 次后")
    print(f"第2代回收: 第1代回收 {thresholds[2]} 次后")

show_gc_mechanism()

对象晋升过程

当对象在垃圾回收中存活下来时,会被晋升到下一代:

  1. 第 0 代回收:存活对象 → 第 1 代
  2. 第 1 代回收:存活对象 → 第 2 代
  3. 第 2 代回收:全量回收,存活对象继续留在第 2 代

这种机制频繁回收年轻对象,减少对长寿命对象的检查。

4.3 分代回收的配置

可以根据应用需求调整分代回收的参数:

python
import gc

def configure_gc():
    # 查看当前配置
    print(f"当前阈值: {gc.get_threshold()}")
    print(f"各代对象数量: {gc.get_count()}")

    # 调整阈值(谨慎操作)
    # gc.set_threshold(1000, 15, 15)  # 提高阈值,减少回收频率

    # 手动触发回收
    collected = gc.collect()
    print(f"手动回收了 {collected} 个对象")

configure_gc()

5. 实际编程建议和最佳实践

5.1 避免循环引用

循环引用会增加垃圾回收的开销:

python
# 不好的实践:创建循环引用
parent = {}
child = {}
parent['child'] = child
child['parent'] = parent  # 创建了循环引用

# 更好的实践:使用弱引用
import weakref
parent = {}
child = {}
parent['child'] = child
child['parent'] = weakref.ref(parent)  # 使用弱引用避免循环引用

# 访问弱引用对象
if child['parent']() is not None:
    print("Parent对象仍然存在")
else:
    print("Parent对象已被回收")

5.2 及时释放大型对象

不再需要的大型对象可以显式赋值为 None,帮助垃圾回收器更快回收内存:

python
# 处理大文件
data = load_large_file()  # 加载大型数据
process(data)  # 处理数据
data = None  # 显式释放引用,允许垃圾回收器回收内存

5.3 使用上下文管理器管理资源

with语句能确保资源在使用后正确释放:

python
# 自动关闭文件
with open('large_file.txt', 'r') as f:
    data = f.read()
    # 处理数据
# 文件会在离开with块时自动关闭

5.4 在性能关键场景调整垃圾回收

对于性能关键的部分,可以临时禁用垃圾回收:

python
import gc
gc_was_enabled = gc.isenabled()
gc.disable()  # 临时禁用
try:
    # 执行性能关键代码...
    pass
finally:
    if gc_was_enabled:
        gc.enable()  # 恢复原状态
    gc.collect()     # 可选:手动回收

5.5 内存泄漏检测

方法 1:使用 tracemalloc 模块(追踪内存分配位置)

python
import tracemalloc
tracemalloc.start()
# ... 执行代码 ...
snapshot = tracemalloc.take_snapshot()
for stat in snapshot.statistics('lineno')[:5]:
    print(stat)

方法 2:使用 gc 模块(监控对象数量)

python
import gc
print(f"各代对象数量: {gc.get_count()}")
print(f"垃圾回收统计: {gc.get_stats()}")
collected = gc.collect()
if gc.garbage:
    print(f"无法回收的对象: {len(gc.garbage)}")

5.6 示例:性能对比与内存检测

python
import gc
import time
import tracemalloc

def performance_comparison():
    """对比开启/关闭GC的性能差异"""
    def workload():
        return sum(range(1000000))

    # 关闭GC测试
    gc.disable()
    start = time.time()
    workload()
    print(f"关闭GC耗时: {time.time() - start:.4f}s")
    gc.enable()

    # 开启GC测试
    gc.collect()
    start = time.time()
    workload()
    print(f"开启GC耗时: {time.time() - start:.4f}s")

def memory_leak_detection():
    """检测内存使用情况"""
    tracemalloc.start()

    # 模拟可能泄漏的代码
    cache = {f"item_{i}": [0] * 100 for i in range(1000)}

    snapshot = tracemalloc.take_snapshot()
    print("\\n[ 内存使用最多的5个位置 ]")
    for stat in snapshot.statistics('lineno')[:5]:
        print(stat)

    tracemalloc.stop()

performance_comparison()
memory_leak_detection()

6. 扩展话题

以下是一些相关的高级话题,有兴趣可以进一步了解:

  • 弱引用(weakref):避免循环引用的高级技术
  • CPython 内存池:小对象缓存和字符串驻留机制
  • del方法陷阱:析构函数的正确使用方式
  • 内存分析工具:objgraph、pympler 等专业工具
  • 不同 Python 实现:CPython、PyPy 等的垃圾回收差异

7. 总结

Python 通过三种机制协同工作,提供安全高效的内存管理:

  • 引用计数:处理大多数常见场景,立即回收不再使用的对象
  • 标记-清除:解决循环引用问题,防止内存泄漏
  • 分代回收:基于对象生存特性,优化回收效率

邬东升的博客