Python 内存管理的核心机制是引用计数。简单说:每个对象内部都有一个计数器,记录着当前有多少个变量或数据结构在引用它。当计数降到 0 时,Python 就自动回收这块内存,程序员几乎无需手动管理。
引用计数的增减规则
理解引用计数,关键是知道什么操作会让计数加 1,什么会让它减 1。
计数增加(+1)的典型场景:
- 创建对象:
x = 3.14,整数对象3.14被x引用,计数为 1。 - 赋值传递:
y = x,y也指向同一个对象,计数变为 2。 - 作为参数传入函数:
func(x)在函数调用期间,形参会增加对象的引用计数。 - 放入容器:
lst.append(x)、dct[key] = x,容器内部多了一个对该对象的引用。
计数减少(-1)的典型场景:
- 变量被重新赋值:
x = 100,原先3.14对象的计数减 1。 - 变量离开作用域:函数执行完毕后局部变量销毁,引用的对象计数减 1。
- 显式删除:
del y,y不再引用该对象,计数减 1。 - 从容器中移除:
lst.remove(x)或容器本身被销毁,内部所有元素的引用计数都会相应减少。
你可以通过 sys.getrefcount() 查看一个对象的当前引用计数(注意它会因为参数传入而临时 +1,所以结果比实际多 1)。
>>> import sys
>>> a = []
>>> sys.getrefcount(a) # 2: a 变量 + 传给 getrefcount 的参数
2
>>> b = a
>>> sys.getrefcount(a) # 3
3
>>> del b
>>> sys.getrefcount(a) # 2
2
引用计数的优势
- 实时回收:对象从可达变为不可达的那一刻,内存就立即被释放,不会等到某个时间点再统一清理。
- 简单、无停顿:没有“垃圾回收器”造成的全局暂停,适合对响应时间敏感的场景。
引用计数的致命缺陷:循环引用
引用计数最大的问题是无法处理循环引用。当两个或多个对象互相引用,形成闭环时,即使外部没有任何变量再指向它们,它们的计数也永远不会归零,导致内存泄漏。
class Node:
def __init__(self, value):
self.value = value
self.next = None
a = Node(1)
b = Node(2)
a.next = b
b.next = a # 形成环
del a
del b # 两个 Node 对象仍然互相引用,计数各为 1,内存无法释放
在这个例子中,a 和 b 被删除后,程序已经没有任何方式访问这两个 Node 对象,但它们内部的 next 属性互相持有引用,导致计数永远为 1。如果没有额外机制,这块内存就“泄漏”了。
循环引用的解决方案
正因为引用计数无法解决循环引用,Python 引入了一个额外的垃圾回收器(GC)专门处理这种情况。它使用“标记-清除”算法定期找出并回收这些孤立但互相引用的对象环。这个补充机制让 Python 的内存管理既保留了引用计数的实时性,又规避了循环引用带来的内存泄漏。
实用要点:
- 写代码时,大多数情况不需要刻意考虑引用计数,Python 会自动打理。
- 重点是要注意循环引用,尤其是在使用了自定义对象互相引用、或者双链表/树等结构时,尽量在不再需要时手动断开引用(比如置为
None),避免给 GC 造成额外负担。 - 对于长时间运行的服务端程序,如果怀疑有内存泄漏,可以借助
tracemalloc模块或objgraph库分析哪些对象没有按预期释放,通常都能定位到循环引用问题。