基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

Python 中的引用计数(Reference Counting)机制是怎样的?

知识点图片

Python 的内存管理机制主要由两部分组成:引用计数(Reference Counting)作为主要机制,标记-清除(Mark and Sweep)分代回收(Generational Collection)作为辅助机制(用于解决循环引用问题)。

这里我们重点详细讲解 引用计数 机制。


1. 什么是引用计数?

在 CPython(Python 的标准实现)中,每一个对象的核心结构体(PyObject)内部都有一个名为 ob_refcnt 的字段。这个字段记录了当前有多少个变量名(引用)指向了这个对象

  • 当计数器变为 0 时,说明没有任何变量引用该对象,Python 就会立即回收该对象占用的内存。

2. 引用计数如何工作?

引用计数增加 (+1) 的情况:

当发生以下操作时,对象的引用计数会增加:

  1. 对象被创建
    python
    a = 328  # 对象 328 被创建,引用计数为 1
  2. 对象被赋值给另一个变量(也就是创建别名):
    python
    b = a    # 328 的引用计数变为 2
  3. 对象作为参数传递给函数
    python
    func(a)  # 在函数运行期间,引用计数 +1(因为函数参数也是引用)
  4. 对象被放入容器(列表、元组、字典等)中
    python
    lst = [a, "hello"]  # 328 的引用计数 +1

引用计数减少 (-1) 的情况:

当发生以下操作时,对象的引用计数会减少:

  1. 使用 del 显式销毁引用
    python
    del a    # 328 的引用计数 -1
  2. 引用被重新赋值
    python
    b = 999  # b 不再指向 328,328 的引用计数 -1
  3. 对象离开作用域
    • 比如函数执行完毕,函数内部的局部变量引用的对象计数会减少。
  4. 容器被销毁或对象从容器中删除
    python
    del lst  # 列表销毁,列表内所有元素的引用计数都会 -1

3. 引用计数的优缺点

优点:

  1. 实时性(Real-time):一旦引用计数归零,内存立即被回收。不需要像 Java 那样等待垃圾回收器(GC)在某个不确定的时间点运行。
  2. 逻辑简单:实现起来相对容易,生命周期清晰。

缺点:

  1. 维护成本高:每次赋值、传参都要更新计数器,这是一笔不小的 CPU 开销。
  2. 无法解决“循环引用”(Cyclic Reference)问题:这是引用计数最大的致命伤。

4. 致命伤:循环引用(Circular Reference)

如果两个对象互相引用,但不再被外部任何变量引用,它们的引用计数永远不会变成 0,导致内存泄漏。

示例:

python
a = []
b = []
a.append(b) # a 引用 b
b.append(a) # b 引用 a

del a
del b

分析:

  1. 执行 del adel b 后,外部没有变量指向这两个列表了。
  2. 但是,列表对象内部互相持有对方的引用,导致它们的 ob_refcnt 都是 1
  3. 因为不为 0,引用计数机制认为它们还在被使用,不会回收
  4. 这部分内存就成了“孤岛”,无法访问也无法释放。

5. Python 如何解决循环引用?

为了解决上述问题,Python 引入了辅助的垃圾回收机制(GC 模块)

  1. 标记-清除(Mark and Sweep)
    • Python 会定期扫描内存中的对象容器(如列表、字典、类实例等,只有容器才可能产生循环引用)。
    • 它会寻找那些“不可达”的对象(即从根节点无法访问到的对象环),并将它们清除。
  2. 分代回收(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(标记清除+分代回收)补丁,专门用来兜底处理引用计数搞不定的循环引用问题。
00:00
00:00