Python 是如何进行内存管理的?
简化版
Python 的内存管理由解释器负责。以 CPython 为例,核心机制是引用计数:对象引用数归零时立即回收;同时配合循环垃圾回收处理对象之间互相引用导致引用数无法归零的问题。开发者通常不用手动释放内存,但要注意循环引用、全局缓存、大对象和资源关闭。
详细版
CPython 常见内存管理点:
- 对象创建后由解释器分配内存。
- 每个对象维护引用计数。
- 引用计数变为 0 时,对象可以被回收。
- 循环引用靠 cyclic garbage collector 检测和清理。
- 小对象有专门的内存分配优化,不等于对象释放后系统内存立刻下降。
示例:
a = []
b = a # 引用增加
del a # 引用减少,但 b 还指向列表
del b # 引用归零,列表可被回收
循环引用:
a = []
b = []
a.append(b)
b.append(a)
即使外部变量删除,a 和 b 内部还互相引用,单靠引用计数无法处理,需要循环 GC。
面试要强调:这是 CPython 的典型实现细节,不应把所有 Python 实现都说成完全一样。
完整版教学
一、先限定:这里主要讲 CPython
Python 语言本身说的是对象模型和语义,具体怎么分配、回收内存取决于解释器实现。面试里最常问的是 CPython,因为它是主流实现:CPython 以引用计数为主,配合循环垃圾回收和内存分配器优化。回答时不要把“CPython 的实现细节”说成“所有 Python 实现都完全一样”。
Python 代码创建对象
|
v
解释器分配对象内存
|
v
CPython 维护引用计数
|
+--> 引用数为 0:对象可立即释放
|
+--> 循环引用:交给循环 GC 检测
这套机制让开发者通常不用手动 free 内存,但不代表不会出现内存问题。全局缓存无限增长、队列堆积、对象互相引用、文件句柄未关闭,都可能让进程资源持续上涨。
二、引用计数是什么
引用计数记录“有多少引用指向这个对象”。
data = [1, 2, 3]
alias = data
列表对象至少被 data 和 alias 两个名字引用。删除一个名字:
del data
对象不会立刻消失,因为 alias 还指向它。
当没有任何引用能访问这个对象时,它就可以被回收。
可以用数字变化理解,但不要在业务代码里依赖具体计数值,因为临时变量、函数调用也会增加引用:
import sys
a = []
print(sys.getrefcount(a)) # 通常比你想的多 1,因为传参也会临时引用
b = a
print(sys.getrefcount(a)) # 增加
del b
print(sys.getrefcount(a)) # 减少
三、引用计数的优点和缺点
优点:
- 回收及时;
- 实现相对直接;
- 对大多数普通对象效果很好。
缺点:
- 维护引用计数有开销;
- 多线程下修改引用计数需要保护,这也是 CPython 历史上引入 GIL 的重要原因之一;
- 处理不了循环引用。
循环引用例子:
class Node:
pass
a = Node()
b = Node()
a.other = b
b.other = a
如果外部再也访问不到 a 和 b,它们仍然互相引用,引用计数不为 0。
引用计数的“及时性”很有用:很多对象一旦最后一个引用消失,很快就能释放。但代价是每次赋值、传参、容器增删都可能维护计数;多线程下维护引用计数还需要同步保护,这也是 CPython 历史上引入 GIL 的原因之一。引用计数还天然处理不了环,所以需要循环 GC 补洞。
四、循环垃圾回收做什么
Python 的循环 GC 会跟踪容器对象,找出只在一组不可达对象内部互相引用的对象图,然后回收它们。
可以通过 gc 模块观察和控制:
import gc
gc.collect()
日常开发很少需要手动调用 gc.collect()。只有在处理大量临时对象、内存峰值敏感、调试内存泄漏时,才会考虑它。
循环引用可以画成这样:
外部变量删除后:
[Node A] ----other----> [Node B]
^ |
|---------other--------|
这一组对象彼此引用,但从程序根对象已经到达不了
CPython 的循环 GC 会关注容器对象,因为简单整数、短字符串这类对象不会形成对象引用环。带 __del__ 的对象、弱引用、扩展类型等场景可能让回收行为更复杂,面试回答不必展开太深,但要知道“引用计数 + 循环 GC”是组合拳。
记忆钩子:引用计数解决“没人指向我”的对象,循环 GC 解决“一群对象只剩彼此互指”的对象,两者处理的是不同形状的垃圾。
五、为什么 del 不等于释放内存给操作系统
del x 删除的是名字绑定,不是强制把对象内存还给操作系统。
x = [1, 2, 3]
del x
如果没有其他引用,对象可以被回收。但解释器可能把内存留在自己的内存池中,用于后续对象分配。因此你可能看到进程占用内存没有马上下降。
这不是一定发生内存泄漏,而可能是内存分配器的复用策略。
CPython 有自己的小对象分配器(常说的 pymalloc)和内存池策略。对象释放后,内存可能回到解释器的池子里,供后续 Python 对象复用,而不是立刻还给操作系统。所以你看到任务管理器里 RSS 没降,不一定代表对象还活着;要结合对象数量、引用链和分配快照分析。
| 现象 | 可能原因 | 怎么排查 |
|---|---|---|
del 后进程内存不降 | 内存池复用 | 看对象是否还可达,别只看 RSS |
| 内存持续上涨 | 缓存/队列/全局引用增长 | 查引用链、限制容量 |
| 循环对象不释放 | 对象环或清理逻辑复杂 | 用 gc、tracemalloc |
| 文件句柄耗尽 | 资源未关闭 | 用 with 和监控 fd |
六、开发中真正要注意什么
常见问题:
- 全局列表、字典、缓存不断增长;
- 闭包或回调持有大对象引用;
- 日志、队列、连接池没有及时清理;
- 文件、socket、数据库连接没有关闭;
- 对象之间复杂循环引用加上自定义清理逻辑导致排查困难。
建议:
- 用
with管理文件和连接; - 缓存设置容量或过期时间;
- 不再需要的大对象及时断开引用;
- 用
tracemalloc、gc、内存分析工具定位泄漏; - 理解“对象不可达”和“进程内存下降”不是一回事。
资源管理要和内存管理分开看。文件、socket、数据库连接这类资源最好用 with 或显式关闭,不能只寄希望于对象析构时机。CPython 引用计数让很多对象看起来“离开作用域就释放”,但跨实现、循环引用或复杂资源对象下,这种依赖会变脆。
with open("data.txt", encoding="utf-8") as f:
data = f.read()
# 离开 with 后文件会关闭,即使中途异常也会走清理逻辑
七、常见误区与追问
- 误区:Python 有 GC,所以一定不会内存泄漏。 如果对象仍被全局缓存、闭包、队列或回调引用,GC 不会回收它。
- 误区:
del x就是释放对象内存。del删除名字绑定;只有对象不可达时才可能回收,进程内存也不一定立刻下降。 - 误区:引用计数能处理所有垃圾对象。 互相引用的对象环需要循环 GC 检测,否则引用计数可能永远不归零。
- 追问:为什么 CPython 的 GIL 和内存管理有关? 引用计数会被频繁修改,GIL 简化了多线程下解释器内部对象状态保护。
- 追问:如何排查内存持续上涨? 先区分对象还活着还是分配器复用,再用
tracemalloc、gc、对象引用链和缓存容量排查。 - 追问:为什么资源释放推荐
with?with把关闭动作绑定到上下文退出,比依赖 GC 或析构时机更稳定。
八、加强记忆
把 CPython 内存管理记成“引用计数负责日常回收,循环 GC 负责对象环,分配器负责复用内存”:引用数归零时对象可回收,互相引用但外部不可达的对象交给循环 GC,del 只是删除名字绑定而不是强制还内存给系统。工程上真正要管的是引用链、缓存增长、大对象生命周期和文件连接这类外部资源。