news 2026/7/13 18:03:37

【python零基础教程第17讲】内存管理与垃圾回收

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【python零基础教程第17讲】内存管理与垃圾回收

Python 内存管理与垃圾回收:从原理到实战排查


一、引言

Python 作为一门高级语言,其自动内存管理机制让开发者从繁琐的malloc/free中解放出来。然而,“自动”并不意味着“无脑”——理解 Python 的内存管理原理,不仅能写出更高效的代码,还能在遇到内存泄漏时快速定位问题。本文将深入剖析 Python 的引用计数、循环引用、分代垃圾回收,并手把手教你使用gc模块进行排查。


二、引用计数:Python 内存管理的基石

2.1 什么是引用计数?

每个 Python 对象内部都有一个ob_refcnt字段,记录当前有多少个引用指向它。当引用计数降为 0 时,对象会被立即销毁,内存被回收。

importsys a=[]# 列表对象引用计数 = 1b=a# 引用计数 = 2print(sys.getrefcount(a))# 输出 3(getrefcount 自身会临时增加一次引用)delb# 引用计数降为 1

2.2 引用计数的优缺点

优点缺点
实时性高,对象不再被引用时立即回收无法处理循环引用
实现简单,无需全局扫描每次赋值、删除都会修改计数,有性能开销
确定性销毁,适合资源管理(如文件句柄)多线程下需要原子操作,增加锁竞争

2.3del删除的是引用,不是对象

del x只是删除变量名x与对象的绑定关系,使对象的引用计数减 1。只有当引用计数归零时,对象才会被回收。

classTest:def__del__(self):print("对象被销毁")t=Test()t2=tdelt# 只删除一个引用,不会触发 __del__print("删除 t 后")delt2# 引用计数归零,触发 __del__

注意__del__方法在对象被回收时调用,但它的执行时机不可预测(尤其在循环引用或异常时),因此不推荐依赖它来释放资源,应使用上下文管理器(with语句)。


三、循环引用:引用计数的“死穴”

3.1 问题场景

当两个或多个对象互相引用时,即使外部没有任何变量指向它们,引用计数也永远不会降为 0,导致内存泄漏。

classNode:def__init__(self,name):self.name=name self.parent=Noneself.child=Nonea=Node("A")b=Node("B")a.child=b b.parent=adeladelb# 此时两个对象引用计数均为 1(互相指向),无法被引用计数机制回收

3.2 循环引用的常见形式

  • 双向链表/树结构:父子节点互相持有引用
  • 闭包:函数内部引用外部变量,外部变量又引用函数
  • 回调/观察者模式:对象注册回调,回调又持有对象引用
  • 异常栈帧:异常对象持有 traceback,traceback 持有局部变量

四、分代垃圾回收:解决循环引用的利器

4.1 分代回收原理

Python 的垃圾回收器(gc模块)专门处理循环引用。它采用**分代(Generational)**策略,将对象分为三代:

  • 第 0 代:新创建的对象
  • 第 1 代:经历一次 GC 后存活的对象
  • 第 2 代:经历多次 GC 后存活的对象

GC 触发频率:第 0 代 > 第 1 代 > 第 2 代。因为大部分对象生命周期很短(如临时变量),频繁扫描年轻代效率更高。

4.2 回收流程(以第 0 代为例)

  1. 标记(Mark):从根对象(全局变量、栈帧中的变量等)出发,遍历所有可达对象,标记为“存活”。
  2. 清除(Sweep):遍历第 0 代所有对象,将未被标记的对象(即不可达的循环引用对象)回收。
  3. 晋升(Promote):本轮存活的对象移入第 1 代。

4.3 阈值与自动触发

importgcprint(gc.get_threshold())# 默认 (700, 10, 10)# 含义:第 0 代对象数量超过 700 时触发第 0 代 GC;# 第 0 代 GC 每进行 10 次,触发一次第 1 代 GC;# 第 1 代 GC 每进行 10 次,触发一次第 2 代 GC。

五、gc模块:手动控制与调试

5.1 常用函数

函数作用
gc.enable()/gc.disable()启用/禁用自动垃圾回收
gc.collect([generation])手动执行回收,返回回收的对象数量
gc.get_objects()返回当前所有被 GC 跟踪的对象列表
gc.get_referrers(obj)返回所有引用obj的对象
gc.get_referents(obj)返回obj引用的所有对象
gc.set_debug(flags)设置调试标志,如gc.DEBUG_LEAK

5.2 手动回收示例

importgc# 关闭自动回收,模拟高负载场景gc.disable()# 创建循环引用classA:def__init__(self):self.b=NoneclassB:def__init__(self):self.a=Nonea=A()b=B()a.b=b b.a=adela,b# 手动触发回收unreachable=gc.collect()print(f"回收了{unreachable}个不可达对象")# 输出 2

5.3 调试内存泄漏

importgc gc.set_debug(gc.DEBUG_LEAK)# 打印所有无法回收的对象# 创建循环引用且包含 __del__ 方法(导致无法回收)classLeak:def__del__(self):passx=Leak()y=Leak()x.ref=y y.ref=xdelx,y gc.collect()# 控制台会输出警告:uncollectable objects

六、内存泄漏场景与排查实战

6.1 常见泄漏场景

  1. 全局缓存未清理:使用listdict作为全局缓存,不断添加数据但从不删除。
  2. 闭包持有大对象:函数内部定义函数,内部函数引用了外部函数的局部变量(如大列表),导致外部函数返回后变量无法释放。
  3. __del__与循环引用:循环引用中的对象定义了__del__方法,GC 无法回收(因为无法确定销毁顺序)。
  4. C 扩展模块:使用ctypesCython时,手动分配的内存未释放。
  5. 线程与threading.local:线程局部变量在线程结束后未清理。
  6. matplotlib等 GUI 库:未关闭的 Figure 对象持续占用内存。

6.2 排查工具与方法

方法一:gc.get_objects()统计对象数量
importgc# 获取所有对象,按类型统计obj_counts={}forobjingc.get_objects():t=type(obj).__name__ obj_counts[t]=obj_counts.get(t,0)+1# 排序输出前 20 个sorted_counts=sorted(obj_counts.items(),key=lambdax:x,reverse=True)forname,countinsorted_counts[:20]:print(f"{name}:{count}")
方法二:objgraph库(第三方)
pipinstallobjgraph
importobjgraph# 显示增长最多的对象objgraph.show_growth(limit=10)# 生成引用关系图(需要 graphviz)objgraph.show_backrefs([my_object],filename="backrefs.png")
方法三:tracemalloc模块(Python 3.4+)
importtracemalloc tracemalloc.start()# 运行你的代码...snapshot=tracemalloc.take_snapshot()top_stats=snapshot.statistics('lineno')forstatintop_stats[:10]:print(stat)
方法四:memory_profiler逐行分析
pipinstallmemory_profiler
@profiledefmy_func():# 你的代码pass

运行:python -m memory_profiler script.py

6.3 实战案例:修复一个泄漏

问题:一个 Web 服务运行几天后内存持续增长,最终 OOM。

排查步骤

  1. 使用gc.get_objects()发现dict对象数量异常多。
  2. 通过objgraph.show_growth()发现RequestContext对象持续增加。
  3. 使用gc.get_referrers()找到持有这些对象的根——一个全局的list缓存。
  4. 定位到代码中未清理的请求日志缓存,添加定期清理逻辑。

修复:将全局list改为collections.deque(maxlen=1000),或使用weakref.WeakValueDictionary


七、最佳实践总结

场景建议
避免循环引用使用weakref模块(弱引用)打破循环
资源管理使用with语句(上下文管理器)而非__del__
大对象缓存设置上限(如 LRU 缓存)或使用弱引用
长期运行服务定期调用gc.collect(2)强制回收老年代
调试阶段开启gc.DEBUG_LEAK或使用tracemalloc
生产环境监控gc.get_count()gc.get_stats()指标

八、结语

Python 的内存管理看似“黑盒”,但通过理解引用计数、分代回收和gc模块,我们完全可以掌控内存的来龙去脉。记住:自动回收不是万能药,良好的编码习惯才是内存安全的根本。希望本文能帮你写出更健壮、更高效的 Python 代码。


如果你有更多内存泄漏的排查经验,欢迎在评论区分享!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/13 18:02:37

AtlasOS终极指南:如何用4个免费工具提升Windows游戏性能30%

AtlasOS终极指南:如何用4个免费工具提升Windows游戏性能30% 【免费下载链接】Atlas 🚀 An open and lightweight modification to Windows, designed to optimize performance, privacy and usability. 项目地址: https://gitcode.com/GitHub_Trending…

作者头像 李华
网站建设 2026/7/13 18:00:31

30分钟构建AI股票分析系统:TradingAgents-CN中文增强版实战指南

30分钟构建AI股票分析系统:TradingAgents-CN中文增强版实战指南 【免费下载链接】TradingAgents-CN 基于多智能体LLM的中文金融交易框架 - TradingAgents中文增强版 项目地址: https://gitcode.com/GitHub_Trending/tr/TradingAgents-CN 在信息过载的金融市场…

作者头像 李华
网站建设 2026/7/13 18:00:10

5分钟革命:NewGAN-Manager如何彻底改变足球经理头像配置体验

5分钟革命:NewGAN-Manager如何彻底改变足球经理头像配置体验 【免费下载链接】NewGAN-Manager A tool to generate and manage xml configs for the Newgen Facepack. 项目地址: https://gitcode.com/gh_mirrors/ne/NewGAN-Manager 在足球经理游戏中&#xf…

作者头像 李华
网站建设 2026/7/13 17:58:21

BilibiliDown:免费跨平台B站视频下载终极指南,3分钟上手!

BilibiliDown:免费跨平台B站视频下载终极指南,3分钟上手! 【免费下载链接】BilibiliDown (GUI-多平台支持) B站 哔哩哔哩 视频下载器。支持稍后再看、收藏夹、UP主视频批量下载|Bilibili Video Downloader 😳 项目地址: https:/…

作者头像 李华
网站建设 2026/7/13 17:58:19

ethereum.rb智能合约部署实战:一步步教你部署Greeter合约

ethereum.rb智能合约部署实战:一步步教你部署Greeter合约 【免费下载链接】ethereum.rb Ethereum library for the Ruby language 项目地址: https://gitcode.com/gh_mirrors/et/ethereum.rb ethereum.rb是Ruby语言的Ethereum开发库,提供了智能合…

作者头像 李华
网站建设 2026/7/13 17:57:57

【软件工程】需求工程:需求获取与需求分析

考点频率:★★★★★(选择题必考,案例分析题高频) 难度:⭐⭐ 建议:重点掌握需求获取与需求分析的区别,记住“获取是‘收’,分析是‘理’”1️⃣ 需求工程在软考中的分量 需求工程是软…

作者头像 李华