Python 中的引用计数(Reference Counting)机制是怎样的?
Python 的内存管理机制主要由两部分组成:引用计数(Reference Counting)作为主要机制,标记-清除(Mark and Sweep)和分代回收(Generational Collection)作为辅助机制(用于解决循环引用问题)。
这里我们重点详细讲解 引用计数 机制。
1. 什么是引用计数?
在 CPython(Python 的标准实现)中,每一个对象的核心结构体(PyObject)内部都有一个名为 ob_refcnt 的字段。这个字段记录了当前有多少个变量名(引用)指向了这个对象。
- 当计数器变为 0 时,说明没有任何变量引用该对象,Python 就会立即回收该对象占用的内存。
2. 引用计数如何工作?
引用计数增加 (+1) 的情况:
当发生以下操作时,对象的引用计数会增加:
- 对象被创建:python
a = 328 # 对象 328 被创建,引用计数为 1 - 对象被赋值给另一个变量(也就是创建别名):python
b = a # 328 的引用计数变为 2 - 对象作为参数传递给函数:python
func(a) # 在函数运行期间,引用计数 +1(因为函数参数也是引用) - 对象被放入容器(列表、元组、字典等)中:python
lst = [a, "hello"] # 328 的引用计数 +1
引用计数减少 (-1) 的情况:
当发生以下操作时,对象的引用计数会减少:
- 使用
del显式销毁引用:pythondel a # 328 的引用计数 -1 - 引用被重新赋值:python
b = 999 # b 不再指向 328,328 的引用计数 -1 - 对象离开作用域:
- 比如函数执行完毕,函数内部的局部变量引用的对象计数会减少。
- 容器被销毁或对象从容器中删除:python
del lst # 列表销毁,列表内所有元素的引用计数都会 -1
3. 引用计数的优缺点
优点:
- 实时性(Real-time):一旦引用计数归零,内存立即被回收。不需要像 Java 那样等待垃圾回收器(GC)在某个不确定的时间点运行。
- 逻辑简单:实现起来相对容易,生命周期清晰。
缺点:
- 维护成本高:每次赋值、传参都要更新计数器,这是一笔不小的 CPU 开销。
- 无法解决“循环引用”(Cyclic Reference)问题:这是引用计数最大的致命伤。
4. 致命伤:循环引用(Circular Reference)
如果两个对象互相引用,但不再被外部任何变量引用,它们的引用计数永远不会变成 0,导致内存泄漏。
示例:
python
a = []
b = []
a.append(b) # a 引用 b
b.append(a) # b 引用 a
del a
del b
分析:
- 执行
del a和del b后,外部没有变量指向这两个列表了。 - 但是,列表对象内部互相持有对方的引用,导致它们的
ob_refcnt都是 1。 - 因为不为 0,引用计数机制认为它们还在被使用,不会回收。
- 这部分内存就成了“孤岛”,无法访问也无法释放。
5. Python 如何解决循环引用?
为了解决上述问题,Python 引入了辅助的垃圾回收机制(GC 模块):
- 标记-清除(Mark and Sweep):
- Python 会定期扫描内存中的对象容器(如列表、字典、类实例等,只有容器才可能产生循环引用)。
- 它会寻找那些“不可达”的对象(即从根节点无法访问到的对象环),并将它们清除。
- 分代回收(Generational Collection):
- 为了提高效率,Python 将对象分为三代(0代、1代、2代)。
- 新创建的对象放入 0 代。如果 0 代经历过一次 GC 扫描还存活,就晋升到 1 代,以此类推。
- 0 代扫描频率最高,2 代最低。这样可以避免频繁扫描那些长期存活的对象。
6. 如何查看引用计数?
你可以使用 sys 模块中的 getrefcount 方法来查看对象的引用计数。
注意:sys.getrefcount(a) 返回的值通常比你预期的多 1,因为该函数在执行时,参数 a 本身也被函数引用了一次(临时引用)。
python
import sys
# 创建对象,计数 = 1
s = "Hello World Ref Count"
print(sys.getrefcount(s))
# 输出通常是 2 (1个是变量s,1个是getrefcount的参数)
b = s
print(sys.getrefcount(s))
# 输出通常是 3 (s, b, getrefcount参数)
del b
print(sys.getrefcount(s))
# 输出通常是 2
总结
- 引用计数是 Python 内存管理的基石,负责处理绝大多数对象的生命周期(一旦归零立即回收)。
- GC(标记清除+分代回收)是补丁,专门用来兜底处理引用计数搞不定的循环引用问题。
右滑查看面试常问