Python GC 垃圾回收

发布时间:2026/7/2 18:19:10
Python GC 垃圾回收 一、什么是垃圾回收在 Python 中一切皆对象。程序运行时会不断创建新对象占用内存空间。如果不及时清理不再使用的对象内存会被逐渐耗尽最终导致程序崩溃。垃圾回收Garbage Collection, GC就是自动识别并回收不再被使用的对象所占用内存的机制。Python 采用引用计数为主标记-清除 分代回收为辅的混合策略。二、核心机制一引用计数Reference Counting2.1 基本原理引用计数是 Python GC 的基石。每个 Python 对象头部都有一个ob_refcnt字段记录当前有多少个地方在引用这个对象。// CPython 底层结构体简化版typedefstruct_object{intob_refcnt;// 引用计数struct_typeobject*ob_type;// 对象类型}PyObject;当对象被新的变量引用时计数 1当引用被销毁时变量离开作用域、del 删除计数 -1当计数变为 0 时对象立即被销毁内存释放2.2 引用计数增减场景操作引用计数变化对象创建a [1, 2, 3]1变量赋值b a1作为函数参数传入1放入容器list.append(a)1变量离开作用域-1del a-1容器被销毁-12.3 代码示例观察引用计数importsys# 创建一个列表对象变量 a 引用它a[1,2,3]print(f创建后引用计数:{sys.getrefcount(a)})# 输出: 2# 注意getrefcount 本身也会临时引用一次所以结果是 2 而不是 1# 新增一个引用baprint(fb a 后引用计数:{sys.getrefcount(a)})# 输出: 3# 放入列表c[a,a]print(f放入列表后引用计数:{sys.getrefcount(a)})# 输出: 5# 删除一个引用delbprint(fdel b 后引用计数:{sys.getrefcount(a)})# 输出: 4# 清空容器c.clear()print(f清空列表后引用计数:{sys.getrefcount(a)})# 输出: 22.4 引用计数的优缺点优点实时性计数归零立即回收没有延迟简单直观逻辑清晰易于理解暂停时间短回收操作分散在程序运行全程缺点额外空间开销每个对象都要维护计数字段维护成本每次赋值、传参都要更新计数致命缺陷无法处理循环引用三、核心机制二标记-清除Mark and Sweep3.1 循环引用问题引用计数最大的痛点就是循环引用。两个对象互相引用即使外部没有任何变量指向它们它们的引用计数也永远不会归零。classNode:def__init__(self,name):self.namename self.nextNoneprint(f创建节点:{name})def__del__(self):print(f销毁节点:{self.name})# 创建循环引用node1Node(A)node2Node(B)node1.nextnode2 node2.nextnode1# 删除外部引用delnode1delnode2# 此时两个对象互相引用计数都不为 0# 引用计数机制无法回收它们print(del 执行完毕但对象还在内存中...)运行上面的代码你会发现__del__并没有被调用。这就是典型的循环引用导致的内存泄漏。3.2 标记-清除算法原理为了解决循环引用Python 引入了标记-清除算法专门针对容器对象列表、字典、类实例等可能引用其他对象的类型。算法分为两个阶段阶段一标记Mark从「根对象」全局变量、调用栈上的变量等出发沿着引用链遍历所有能到达的对象标记为「可达」无法到达的对象就是垃圾阶段二清除Sweep遍历所有对象清除未被标记的对象对于循环引用形成的「孤岛」由于从根对象无法到达会被全部回收垃圾回收是存在 Stop-The-WorldSTW全局停顿 机制的核心的标记 - 清除分代回收过程是完全的STW模式日常的引用计数回收是分散式开销不属于传统意义上的集中式 STW 停顿。3.3 代码示例手动触发 GC 回收循环引用importgcclassNode:def__init__(self,name):self.namename self.nextNoneprint(f创建节点:{name})def__del__(self):print(f销毁节点:{self.name})# 先禁用自动 GC方便观察gc.disable()print( 创建循环引用 )node1Node(A)node2Node(B)node1.nextnode2 node2.nextnode1delnode1delnode2print( 已删除外部引用对象未回收 )# 手动触发垃圾回收print( 手动执行 gc.collect() )collectedgc.collect()print(f回收了{collected}个对象)运行结果 创建循环引用 创建节点: A 创建节点: B 已删除外部引用对象未回收 手动执行 gc.collect() 销毁节点: A 销毁节点: B 回收了 2 个对象可以看到执行gc.collect()后循环引用的对象被成功回收。四、核心机制三分代回收Generational Collection4.1 分代思想标记-清除虽然能解决循环引用但每次都要扫描所有对象效率很低。分代回收基于一个统计学规律对象存活时间越短越可能是垃圾存活时间越长越可能继续存活。这就是「弱代假说」。Python 把对象分成 3 代越年轻的代扫描越频繁越老的代扫描越少。4.2 三代划分代别名称特点第 0 代年轻代新创建的对象都在这里回收最频繁第 1 代中年代经历过 0 代回收后存活下来的对象第 2 代老年代经历过 1 代回收后存活回收最少4.3 阈值机制Python 通过计数器和阈值控制回收时机importgc# 查看默认阈值 (threshold0, threshold1, threshold2)print(gc.get_threshold())# 默认输出: (700, 10, 10)含义threshold0 7000代对象数量累计达到 700 时触发 0 代回收threshold1 10每 10 次 0 代回收触发 1 次 1 代回收threshold2 10每 10 次 1 代回收触发 1 次 2 代回收4.4 代码示例观察分代计数变化importgc# 查看当前各代计数print(初始计数:,gc.get_count())# (count0, count1, count2)# 创建大量对象foriinrange(1000):obj{index:i,data:list(range(10))}print(创建1000个对象后:,gc.get_count())# 手动触发各代回收gc.collect(0)# 只回收 0 代print(回收0代后:,gc.get_count())gc.collect(1)# 回收 01 代print(回收1代后:,gc.get_count())gc.collect(2)# 全量回收默认print(回收2代后:,gc.get_count())4.5 调整阈值进行性能调优importgc# 提高 0 代阈值减少 GC 频率适合内存充足、追求速度的场景gc.set_threshold(1000,15,10)# 降低阈值更频繁回收适合内存紧张的场景# gc.set_threshold(300, 5, 5)print(调整后阈值:,gc.get_threshold())五、gc 模块常用 API 大全5.1 基础控制importgc# 启用/禁用自动 GCgc.enable()# 启用默认开启gc.disable()# 禁用gc.isenabled()# 检查是否启用# 手动触发回收gc.collect()# 全量回收2代返回回收对象数gc.collect(0)# 只回收 0 代gc.collect(1)# 回收 01 代gc.collect(2)# 回收 012 代等同于默认5.2 阈值与统计# 获取/设置阈值gc.get_threshold()# 返回 (t0, t1, t2)gc.set_threshold(t0,t1,t2)# 获取当前计数gc.get_count()# 返回 (count0, count1, count2)# 获取详细统计信息statsgc.get_stats()fori,statinenumerate(stats):print(f第{i}代: 回收{stat[collections]}次, 回收了{stat[collected]}个对象)5.3 对象追踪与调试# 获取所有被 GC 追踪的对象all_objectsgc.get_objects()print(f当前被追踪的对象数:{len(all_objects)})# 查找引用了某个对象的所有容器target[1,2,3]referrersgc.get_referrers(target)print(f引用 target 的对象有:{len(referrers)}个)# 开启调试模式检测无法回收的对象gc.set_debug(gc.DEBUG_LEAK)5.4 回调钩子defgc_callback(phase,info):ifphasestart:print(fGC 开始代:{info[generation]})else:print(fGC 结束回收了{info[collected]}个对象)gc.callbacks.append(gc_callback)# 触发一次 GC 观察回调gc.collect()六、常见内存泄漏场景与解决方案6.1 场景一循环引用问题对象互相引用形成环引用计数永远不为 0。解决方案使用weakref弱引用不增加引用计数。importweakrefclassNode:def__init__(self,name):self.namename self._nextNonepropertydefnext(self):# 弱引用需要调用才能获取原对象returnself._next()ifself._nextelseNonenext.setterdefnext(self,node):# 使用弱引用持有下一个节点self._nextweakref.ref(node)def__del__(self):print(f销毁:{self.name})# 测试node1Node(A)node2Node(B)node1.nextnode2 node2.nextnode1delnode1delnode2# 此时对象会被立即回收不需要 GC 介入print(对象已正常回收)6.2 场景二全局缓存无限膨胀问题全局字典/列表作为缓存只加不清理。解决方案使用functools.lru_cache或手动设置上限。fromfunctoolsimportlru_cache# 设置最大缓存条目数超过自动淘汰lru_cache(maxsize128)defexpensive_func(n):print(f计算{n}...)returnn**26.3 场景三闭包意外持有引用问题闭包捕获了大对象导致无法释放。解决方案手动解除引用或只捕获必要的值。defcreate_handler(big_data):# 只提取需要的值不要持有整个 big_dataconfigbig_data[config]defhandler():returnconfigreturnhandler七、最佳实践总结优先依赖自动 GC默认机制已经足够好不要频繁手动调用gc.collect()避免循环引用设计数据结构时尽量避免环必要时使用weakref及时释放大对象超大对象用完后delgc.collect()主动释放合理调整阈值内存充足可提高阈值减少 GC 次数内存紧张则降低阈值使用工具排查泄漏tracemalloc、objgraph等工具定位内存泄漏慎用__del__析构函数会延迟 GC 回收还可能导致对象进入gc.garbage无法回收八、一句话总结Python 的垃圾回收 引用计数日常主力实时回收 标记清除专门解决循环引用 分代回收按年龄分层扫描提升效率。三者协同工作在保证内存安全的同时尽可能降低性能开销。