Python 的每个对象内部都有一个计数器,记录当前有多少个引用指向这个对象。这个计数器就叫引用计数。当引用计数变为零时,对象就会被立即回收,内存被释放。理解增减规则能帮你写出更稳、更省内存的代码。
引用计数增加的情况
以下操作会让对象的引用计数 +1:
- 创建对象
即便没人赋值,对象刚诞生时引用计数就是 1。
a = [] # 列表对象引用计数 = 1
- 新的引用指向该对象
b = a # a 和 b 指向同一个列表,引用计数变为 2
- 将对象放入容器
lst = [a] # 列表 lst 内部持有对 a 所指数列表的引用,引用计数 +1 → 3
dic = {'key': a}
- 作为函数参数传递
调用函数时,实参会临时让形参指向同一个对象,引用计数短暂增加。
def foo(x): # x = a,引用计数 +1
...
foo(a) # 进入函数时 a 引用计数 +1,函数返回后恢复
- 作为另一个对象的属性
obj.data = a # obj.data 也指向同一对象,引用计数 +1
引用计数减少的情况
以下操作会让引用计数 -1:
- 引用被销毁(离开作用域)
比如函数返回后,函数内部的局部变量会被销毁,引用计数对应减少。
def bar():
x = [1, 2] # 进入函数时引用计数 1
bar() # 返回后 x 销毁,列表对象引用计数归零,立即回收
- 引用被重新指向其他对象
a = [1, 2] # 列表 [1,2] 引用计数 1
a = "hello" # a 现在指向字符串,原来 [1,2] 引用计数 -1
- 容器对象被移除或销毁
lst = [a] # a 指向的对象引用计数 +1
lst = None # lst 被置空,内部元素 a 的引用释放,引用计数 -1
- 显式调用
del
del a # 删除变量 a,它原来指向的对象引用计数 -1
实际中要注意的细节
- 不可变类型与对象复用
小整数、短字符串等不可变对象可能会被 Python 内部缓存,即使引用计数降到零也不一定会立刻释放。例如:
a = 10
b = 10 # 可能指向同一个整数对象 10,引用计数会大于 1
这是 Python 的一种优化,日常编码不必刻意关注。
- 循环引用会让引用计数失效
如果两个对象互相引用,即使外界已经没有变量指向它们,它们的引用计数也永远不会归零,从而导致内存泄漏。这就是为什么垃圾回收还需要标记-清除和分代回收来辅助。典型案例:
a = []
b = []
a.append(b) # b 中的列表引用计数 +1
b.append(a) # a 中的列表引用计数 +1
del a
del b # 两个对象互相持有引用,引用计数仍为 1,不会被引用计数机制回收
- 很多内置操作会临时增加引用计数
例如使用 sys.getrefcount() 查看计数时,函数调用本身会造成引用计数的临时增加,所以看到的值通常比你预期的多 1。
理解增减规则,能帮你更清楚地判断对象何时会被销毁,也有助于在使用自定义容器、文件、网络连接等资源时,避免出现“明明不用了但内存迟迟不释放”的困惑。