在上次的内容中,我们看了python中的对象,今天我们来看看Python内存管理的核心机制--引用计数。这是一种简单但是非常强大的机制,python会给每个对象,维护一个引用计数器,用来跟踪当前有多少个引用指向了这个对象。它的工作原理很简单,当给变量复制、将对象作为参数传入函数,将对象存入列表/字典等数据结构时,Python会增加该对象的引用计数;当删除变量,变量超出作用域或者将对象从数据结构中移除时,就会减少该对象的引用计数。当引用计数降到0时,则说明这个对象已经没有人在使用了,python会立即回收这块内存。Python的sys模块,提供了sys.getrefcount()函数,可以让我们直接看到对象的引用计数。import sysx = y = 2**123 # x和y指向同一个对象z = 2**123 # z指向另一个对象(大整数无缓存)print(f"{x is y=}") # True - 同一个对象print(f"{sys.getrefcount(x)=}") # 3个引用(x、y + 函数参数临时引用)print(f"{sys.getrefcount(y)=}") # 同样是3(和x指向同一个对象)print(f"{x is z=}") # False - 不同对象print(f"{sys.getrefcount(z)=}") # 2个引用(z + 函数参数临时引用)
x is y=Truesys.getrefcount(x)=3sys.getrefcount(y)=3x is z=Falsesys.getrefcount(z)=2
让我们来分析下上面的代码:首先x、y指向同一个对象,这会给它的引用加2,调用sys.getrefcount(x)时,x作为参数,临时增加一个引用,所以引用数是2+1=3;z指向另一个对象,只有它自己加上函数参数的临时引用,因此总共有2个。import sysx = 4print(f"{sys.getrefcount(x)=}") # 输出不是2或3,而是一个超大数(比如3221225472)?!
import sysfor x in range(5): print(f"for {x=}:") print(f" {sys.getrefcount(x)=}") print(f" {id(x)=}")
运行后会发现:整数0~4的引用计数,都是同一个超大数。我们把这个数转换成十六进制和二进制,就能找到答案:
import sysx = 4print(f"{sys.getrefcount(x)=:x}") # 输出 0x13a(十六进制)print(f"{sys.getrefcount(x)=:b}") # 输出 100111010(二进制)
它并不是一个真实的引用计数,而是python用来标记“不朽对象”的标志。Python有一个隐藏的优化:有些对象会被标记为“不朽对象(Immortal Objects)”——它们会伴随整个程序的生命周期,永远不会被垃圾回收,Python也不会为它们维护真实的引用计数(而是用 0x13a 这样的标志代替)。
那么哪些对象是不朽对象呢?主要有上面提到的小整数(-5~256),单例对象例如None、True、False,空的元组。除了不朽对象,Python对字符串也有类似的优化——字符串驻留(String Interning)。Python会缓存一些常用的字符串(比如标识符、短字符串),让它们成为驻留对象,避免重复分配内存。
对于字符串驻留的细节,有兴趣可以翻翻Python的官方文档,这里就不再啰嗦了。
下次我们会看看Python对象在内存中如何存储