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 创建属性引用

引用计数减少的情况:

  • 变量重新赋值a = new_obj 原对象引用计数减1
  • 变量离开作用域:函数结束时局部变量被销毁
  • 容器删除:从列表、字典中删除对象
  • 显式删除:使用del语句删除变量

引用计数归零时的处理:

  • Python立即释放对象占用的内存
  • 如果对象引用了其他对象,那些对象的引用计数也会减1
python
import sys

def detailed_refcount_example():
    # 演示不同操作对引用计数的影响
    
    # 1. 创建对象
    data = [1, 2, 3, 4, 5]  # 引用计数 = 1
    print(f"创建后引用计数: {sys.getrefcount(data) - 1}")
    
    # 2. 变量赋值增加引用计数
    data_ref = data  # 引用计数 = 2
    print(f"变量赋值后: {sys.getrefcount(data) - 1}")
    
    # 3. 放入容器增加引用计数  
    container = {'list': data}  # 引用计数 = 3
    print(f"放入字典后: {sys.getrefcount(data) - 1}")
    
    # 4. 函数调用临时增加引用计数
    def process_list(lst):
        # 在函数内部,lst参数增加了引用计数
        print(f"函数内部引用计数: {sys.getrefcount(lst) - 1}")
        # 函数结束后,参数引用被释放
    
    process_list(data)
    print(f"函数调用后: {sys.getrefcount(data) - 1}")
    
    # 5. 删除引用减少引用计数
    del data_ref  # 引用计数 = 2
    print(f"删除data_ref后: {sys.getrefcount(data) - 1}")
    
    del container['list']  # 引用计数 = 1
    print(f"从字典删除后: {sys.getrefcount(data) - 1}")
    
    # 6. 最后一个引用
    print("即将删除最后一个引用...")
    del data  # 引用计数 = 0,对象被立即回收

detailed_refcount_example()

2.2 引用计数示例

  • 使用sys.getrefcount(xxx) 获取引用计数
python
import sys

def reference_count_example():
    # 创建一个列表对象,引用计数为1
    a = [1, 2, 3]
    
    # sys.getrefcount()返回的计数比实际多1,因为传参时创建了临时引用
    print(f"引用计数(a): {sys.getrefcount(a) - 1}")  # 输出: 1
    
    # b也指向同一对象,引用计数增加到2
    b = a
    print(f"引用计数(a): {sys.getrefcount(a) - 1}")  # 输出: 2
    
    # 创建一个引用这个列表的字典
    c = {'list': a}
    print(f"引用计数(a): {sys.getrefcount(a) - 1}")  # 输出: 3
    
    # 删除b的引用,计数减1
    del b
    print(f"引用计数(a): {sys.getrefcount(a) - 1}")  # 输出: 2
    
    # 删除c的引用,计数减1
    del c
    print(f"引用计数(a): {sys.getrefcount(a) - 1}")  # 输出: 1
    
    # 当函数结束时,a离开作用域,引用计数减为0,对象被回收

reference_count_example()

2.3 引用计数的优缺点

优点:

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

缺点:

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

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)

标记阶段从根对象开始遍历。根对象是确定还在使用的对象,包括:

  • 全局变量:模块级别的变量
  • 局部变量:当前函数栈中的变量
  • 寄存器中的对象:正在被CPU使用的对象
  • 其他活跃对象:如异常对象、线程局部数据等

标记过程:

  1. 从根对象开始,标记为"可达"
  2. 递归访问该对象引用的所有其他对象
  3. 标记每个访问到的对象
  4. 重复这个过程,直到所有可达对象都被标记
python
# 标记阶段的概念示例
def mark_phase_concept():
    # 假设有以下对象关系
    global_var = {'name': 'root'}        # 根对象1
    local_var = [1, 2, 3]               # 根对象2
    
    # global_var引用了其他对象
    global_var['data'] = local_var       # local_var被标记为可达
    global_var['nested'] = {'key': 'value'}  # nested字典被标记为可达
    
    # 循环引用的对象
    a = {}
    b = {}
    a['ref'] = b
    b['ref'] = a
    # 由于a和b都没有被根对象引用,它们不会被标记为可达

清除阶段(Sweep Phase)

清除阶段遍历所有对象,检查标记状态:

  1. 未标记的对象:标记阶段没访问到,说明不可达,可以回收
  2. 已标记的对象:清除标记,为下次垃圾回收做准备

循环引用的检测原理

标记-清除算法能解决循环引用问题,关键在于它不看引用计数,而是看对象是否可达:

python
def cycle_detection_example():
    # 创建循环引用
    node1 = {'data': 'A'}
    node2 = {'data': 'B'} 
    node1['next'] = node2  # node1 -> node2
    node2['next'] = node1  # node2 -> node1 (循环引用)
    
    # 即使存在循环引用,如果没有外部引用指向node1或node2
    # 标记阶段从根对象开始遍历时,无法到达这两个对象
    # 因此它们不会被标记,最终在清除阶段被回收
    
    return node1  # 如果返回node1,它就变成可达的了

算法的时间复杂度

  • 标记阶段:O(N),其中N是可达对象的数量
  • 清除阶段:O(M),其中M是所有对象的数量
  • 总体复杂度:O(N + M)

优点是能处理任意复杂的循环引用,缺点是需要暂停程序执行,可能造成明显的停顿。

3.3 gc模块基础使用

gc模块提供了控制循环引用垃圾回收的接口。最常用的功能:

基本控制功能

python
import gc

# 1. 启用/禁用垃圾回收
print(f"垃圾回收器状态: {gc.isenabled()}")
gc.disable()  # 禁用
gc.enable()   # 启用

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

# 3. 获取统计信息
counts = gc.get_count()
print(f"各代对象数量: {counts}")
thresholds = gc.get_threshold()
print(f"回收阈值: {thresholds}")

调试循环引用

python
import gc

def debug_cycles():
    # 设置调试模式
    gc.set_debug(gc.DEBUG_LEAK)
    
    # 创建循环引用
    a = {}
    b = {}
    a['b'] = b
    b['a'] = a
    del a, b
    
    # 手动回收并查看结果
    collected = gc.collect()
    print(f"回收了 {collected} 个对象")
    
    # 检查无法回收的对象
    if gc.garbage:
        print(f"无法回收的对象: {len(gc.garbage)}")
    
    # 重置调试设置
    gc.set_debug(0)

debug_cycles()

3.4 使用gc模块测试标记清楚

gc.collect() 不仅仅是执行一次“标记-清除” —— 它是按“分代回收机制”触发的完整垃圾回收过程,会从最年轻代(第0代)开始,逐级向上判断是否需要回收。

使用gc模块来监控和优化内存使用:

python
import gc
import time

def demonstrate_cycle_collection():
    print("=== 循环引用垃圾回收演示 ===")
    print("创建循环引用前")
    print(f"垃圾收集器状态: {'启用' if gc.isenabled() else '禁用'}")
    
    def create_cycle():
        # 创建循环引用
        a = {}
        b = {}
        a['b'] = b
        b['a'] = a
    
    # 禁用自动垃圾回收以便观察
    gc.disable()
    
    # 创建循环引用
    create_cycle()
    
    print("\n创建循环引用后,手动回收前")
    print(f"当前收集器统计: {gc.get_count()}")
    
    # 手动触发回收
    collected = gc.collect()
    
    print("\n手动回收后")
    print(f"回收的对象数量: {collected}")
    
    # 重新启用自动垃圾回收
    gc.enable()

demonstrate_cycle_collection()

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
import time


def performance_critical_function_disable_gc():
    # 临时禁用垃圾回收
    gc_was_enabled = gc.isenabled()
    gc.disable()

    try:

        time_start = time.time()
        value = 0
        # 执行性能关键代码
        for i in range(1000000):
            value += i
            # print(value)
        time_end = time.time()
        print("关闭垃圾回收,执行时间:", time_end - time_start)
    finally:
        # 恢复原来的垃圾回收状态
        if gc_was_enabled:
            gc.enable()

        # 可选:手动触发一次回收
        gc.collect()

# 开启垃圾回收测试


def performance_critical_function_enable_gc():
    try:

        time_start = time.time()

        value = 0
        # 执行性能关键代码
        for i in range(1000000):
            value += i
            # print(value)
        time_end = time.time()
        print("开启垃圾回收,执行时间:", time_end - time_start)
    finally:
        # 可选:手动触发一次回收
        gc.collect()


performance_critical_function_disable_gc()
performance_critical_function_enable_gc()

5.5 内存泄漏检测的两种主要方法

方法1:使用tracemalloc模块

  • 判断每个地方使用了多少内存!
python
import tracemalloc

# 开始追踪
tracemalloc.start()

# 执行可能导致内存泄漏的代码
def potential_leak():
    cache = {}
    for i in range(10000):
        key = f"item_{i}"
        cache[key] = [0] * 1000

potential_leak()

# 获取当前内存快照
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')

print("[ 内存使用最多的10个位置 ]")
for stat in top_stats[:10]:
    print(stat)

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

python
import gc

def monitor_objects():
    # 获取垃圾回收统计
    print(f"各代对象数量: {gc.get_count()}")
    print(f"垃圾回收统计: {gc.get_stats()}")
    
    # 检查是否有无法回收的对象
    collected = gc.collect()
    print(f"手动回收的对象数量: {collected}")
    
    if gc.garbage:
        print(f"无法回收的对象: {len(gc.garbage)}")

monitor_objects()

6. 扩展话题

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

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

7. 总结

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

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

邬东升的博客