内存管理与垃圾回收
难度:⭐⭐ | 高频指数:🔥🔥🔥
面试回答
常见问法
- Python 怎么管理内存?
- 引用计数有什么缺陷?怎么解决循环引用?
- 分代垃圾回收是什么原理?
__del__什么时候会被调用?有什么坑?weakref弱引用有什么用?- 和 C++ 的 RAII / shared_ptr 有什么异同?
回答
Python 内存管理分三层:私有堆 + 引用计数 + 分代 GC。
-
引用计数是主力机制:每个对象维护一个引用计数器,当计数归零时立即释放。优点是实时回收、延迟低;缺点是无法处理循环引用,且维护计数有额外开销。
-
分代垃圾回收(Generational GC)专门解决循环引用:将对象分为 0/1/2 三代,新对象在第 0 代,存活越久晋升到更高代。GC 频率:第 0 代最频繁,第 2 代最少。
-
pymalloc:Python 自带的小对象内存池,对 ≤512 字节的对象使用 arena/pool/block 三级结构,减少系统调用。
一句话总结:引用计数管日常释放,分代 GC 兜底循环引用,pymalloc 优化小对象分配。
追问
1)引用计数怎么观察?
import sys
a = [1, 2, 3]
print(sys.getrefcount(a)) # 2(a 本身 + getrefcount 参数)
b = a
print(sys.getrefcount(a)) # 3
del b
print(sys.getrefcount(a)) # 2
注意 sys.getrefcount() 本身会临时增加一次引用。
2)什么操作会增加/减少引用计数?
增加:赋值、传参、放入容器、函数闭包捕获。
减少:del、变量重新赋值、离开作用域、从容器移除。
3)循环引用怎么产生的?
class Node:
def __init__(self):
self.parent = None
self.children = []
a = Node()
b = Node()
a.children.append(b)
b.parent = a # a -> b -> a,循环引用
del a, b # 引用计数不会归零,需要 GC 介入
4)分代 GC 怎么工作?
import gc
# 查看各代阈值
print(gc.get_threshold()) # 默认 (700, 10, 10)
# 含义:第 0 代分配 700 次触发 GC;第 0 代 GC 10 次后触发第 1 代;以此类推
# 手动触发
gc.collect() # 返回回收的不可达对象数量
# 查看某对象的引用者
gc.get_referrers(obj)
5)__del__ 有什么坑?
class Resource:
def __del__(self):
print("释放资源")
# 坑1:不保证调用时机(解释器退出时可能不调用)
# 坑2:循环引用时,GC 无法确定析构顺序,可能不调用 __del__
# 坑3:__del__ 里抛异常会被忽略
# 坑4:复活对象(在 __del__ 里把 self 赋给全局变量)
工程建议:不要依赖 __del__ 管理资源,用上下文管理器(with)代替。
6)weakref 弱引用有什么用?
import weakref
class Cache:
pass
obj = Cache()
ref = weakref.ref(obj)
print(ref()) # <Cache object>
del obj
print(ref()) # None,不阻止 GC 回收
典型场景:
- 缓存(不希望缓存阻止对象被回收)
- 观察者模式(避免观察者和被观察者循环引用)
WeakValueDictionary/WeakSet
7)和 C++ RAII / shared_ptr 的类比?
| 维度 | Python | C++ |
|---|---|---|
| 主力机制 | 引用计数 + GC | RAII + 智能指针 |
| 循环引用 | 分代 GC 自动处理 | weak_ptr 手动打破 |
| 释放时机 | 引用归零立即释放(多数情况) | 离开作用域立即析构 |
| 析构保证 | __del__ 不保证 | 析构函数保证调用 |
| 资源管理最佳实践 | with 上下文管理器 | RAII |
原理展开
1. 三层内存架构
┌─────────────────────────────────────┐
│ Layer 3: Object-specific allocators │ ← list, dict, tuple 各自的 free list
├─────────────────────────────────────┤
│ Layer 2: Python object allocator │ ← pymalloc (≤512B)
├─────────────────────────────────────┤
│ Layer 1: Python raw memory allocator│ ← malloc/free 封装
├─────────────────────────────────────┤
│ Layer 0: OS memory allocator │ ← 系统调用 (mmap/brk)
└─────────────────────────────────────┘
pymalloc 的 arena(256KB)→ pool(4KB,按大小分类)→ block(8~512B)结构,避免频繁 malloc/free。
2. 引用计数的实现
CPython 中每个对象头部都有 ob_refcnt 字段:
// CPython 源码简化
typedef struct {
Py_ssize_t ob_refcnt; // 引用计数
PyTypeObject *ob_type; // 类型指针
} PyObject;
每次赋值、传参等操作都会 Py_INCREF / Py_DECREF。当 ob_refcnt 降为 0,立即调用 tp_dealloc 释放。
3. 分代 GC 的标记-清除算法
GC 处理循环引用的核心步骤:
- 遍历第 N 代所有容器对象(只有容器才可能循环引用)
- 复制引用计数到临时字段
gc_refs - 模拟删除:遍历每个对象的引用,将被引用对象的
gc_refs减 1 - 标记:
gc_refs > 0的对象从外部可达,标记为存活 - 清除:
gc_refs == 0且不可达的对象,判定为垃圾
import gc
# 禁用自动 GC(性能敏感场景)
gc.disable()
# 手动在合适时机触发
gc.collect(generation=0) # 只收集第 0 代
gc.collect() # 收集所有代
4. 内存泄漏排查实战
import tracemalloc
# 启动内存追踪
tracemalloc.start()
# ... 业务代码 ...
# 查看内存快照
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
for stat in top_stats[:10]:
print(stat)
配合 gc.set_debug(gc.DEBUG_LEAK) 可打印无法回收的对象;objgraph 库可可视化引用关系。
易错点
-
以为
del就是释放内存del只是解除名字绑定、减少引用计数,不一定立即释放(可能还有其他引用)。 -
依赖
__del__做资源清理 不保证调用时机,循环引用时行为不确定。应该用with语句。 -
以为 Python 没有内存泄漏 循环引用 + 全局变量持有 + 闭包捕获,都可能导致对象无法回收。
-
混淆”引用计数归零”和”GC 回收” 引用计数归零是立即释放;GC 是周期性扫描处理循环引用。
-
在多线程中手动调
gc.collect()GC 会暂停所有线程(STW),高频调用影响性能。 -
以为
gc.disable()后不会有任何回收 引用计数仍然工作,只是分代 GC 不再自动触发。
记忆技巧
-
三层口诀:pymalloc 管分配,引用计数管释放,分代 GC 管循环。
-
引用计数像投票:每多一个人引用就 +1,没人引用就淘汰。
-
分代像学校:新生(gen0)考试最频繁,老生(gen2)很少被查。
-
__del__像遗嘱:不保证什么时候执行,别把重要事放里面。 -
weakref 像旁观者:看得到对象,但不会阻止它被回收。
面试速答版
Python 内存管理核心是引用计数 + 分代垃圾回收。引用计数是主力,对象引用归零立即释放,优点是实时性好;缺点是无法处理循环引用。分代 GC 专门解决循环引用,把对象分为 0/1/2 三代,新对象在第 0 代,GC 频率从高到低。底层还有 pymalloc 内存池优化小对象分配。工程上不要依赖 __del__ 管理资源,用上下文管理器;排查内存泄漏用 tracemalloc + gc 模块。和 C++ 对比,Python 的引用计数类似 shared_ptr,分代 GC 相当于自动版的 weak_ptr 打破循环。