GC机制
Python 的垃圾回收机制、循环引用、gc 模块、弱引用等
1. 垃圾回收机制
Python 的垃圾回收机制以 引用计数为主,分代收集为辅
若一个对象的引用计数为 0,Python 虚拟机就会回收该对象的内存,如下是一个手动销毁对象触发的垃圾回收过程
class ClassA:
def __init__(self):
print(f"object born, id:{hex(id(self))}")
def __del__(self):
print(f"object del, id:{hex(id(self))}")
def f1():
c1 = ClassA()
del c1
f1()
### 输出结果
object born, id:0x1ee7a5d5580
object del, id:0x1ee7a5d5580
当执行 c1 = ClassA()
时,就会创建一个对象,放在一块内存中,c1
变量指向这块内存,此时这块内存如 0x1ee7a5d5580
的引用就是 1
del c1
后,c1
变量不再指向 0x1ee7a5d5580
内存,所以这块内存的引用计数 -1 等于 0 ,所以就销毁了这个对象,然后释放内存
1.1 引用计数
引用计数+1 场景
对象被创建,如:
a = 23
被引用,如:
b = a
对象被作为参数,传入到一个函数中,如:
func(a)
作为一个元素,存储在容器中,如
list1 = [a, a]
引用计数-1 场景
- 对象的别名被显式销毁,如
del a
- 对象的别名被赋予新的对象,如
a = 24
- 一个对象离开它的作用域,如:
func()
执行完毕时,func()
中的局部变量 (全局变量、子线程、协程等不同 ) - 对象所在的容器被销毁,或从容器中删除对象
查看对象引用计数
sys.getrefcount(object) -> int
- 参数:待查看的对象
class ClassA:
def __init__(self):
print(f"object born, id:{hex(id(self))}")
def __del__(self):
print(f"object del, id:{hex(id(self))}")
c1 = ClassA()
d = c1
print(sys.getrefcount(c1)) # 3
del c1
查看 c1 对象的引用计数为3 ,比预期引用计数多 1,这是由于调用 sys.getrefcount
函数时又传入 c1, 因此引用 +1
2.1 循环引用
循环引用是很严重的问题,它会导致内存泄露
class ClassA:
def __init__(self):
print(f"object born, id:{hex(id(self))}")
def __del__(self):
print(f"object del, id:{hex(id(self))}")
def f2():
c1 = ClassA()
c2 = ClassA()
c1.t = c2
c2.t = c1
del c1
del c2
f2()
print(f"globals: {globals()}")
### 输出结果
object born, id:0x2726f4155b0
object born, id:0x2726f415040
globals: {... 'sys': <module 'sys' (built-in)>, 'ClassA': <class '__main__.ClassA'>, 'f2': <function f2 at 0x000002726F42B550>}
object del, id:0x2726f4155b0
object del, id:0x2726f415040
此时发现,再调用完 f2()
后,也依然没有触发 del c1
和 del c2
,此时全局变量中依然可以发现对 c1
和 c2
的引用
这是由于 执行 c1.t = c2
和 c2.t = c1
后,这两块内存相互引用,引用计数变为了 2
- 在
del c1
后,内存中 c1对象 的引用计数变为 1,由于不为 0,所以内存中 c1对象 不会被销毁 - 所以
del c2
同理 - 即使上述两对象都是可以被销毁的,但由于循环引用,导致垃圾回收器不会回收,故引发内存泄露
2. 垃圾回收(gc)
Python 的垃圾回收有 **gc (Garbage Collector interface ) ** 模块,它提供一个接口给开发者设置垃圾回收的选项
上文采用引用计数的方法管理内存的一个缺陷是 循环引用,而 gc 模块其中一个主要功能,就是解决循环引用的问题
Python 有三种情况会触发垃圾回收
- 调用
gc.collect()
- 当 gc 模块的计数器达到阀值的时候
- 程序退出时
示例:
import gc
class ClassA:
def __init__(self):
print(f"object born, id:{hex(id(self))}")
def __del__(self):
print(f"object del, id:{hex(id(self))}")
def f3():
c1 = ClassA()
c2 = ClassA()
c1.t = c2
c2.t = c1
del c1
del c2
time.sleep(1)
print(f"当前垃圾回收列表:{gc.garbage}\n")
print(f"显式执行垃圾回收, 回收引用: {gc.collect()}\n")
print(f"显式执行后的回收列表:{gc.garbage}\n")
time.sleep(10)
if __name__ == '__main__':
gc.set_debug(gc.DEBUG_LEAK) # 设置gc模块的日志
f3()
### 输出结果
object born, id:0x146dfe04fd0
object born, id:0x146dfe04fa0
gc: collectable <ClassA 0x00000146DFE04FD0>
gc: collectable <ClassA 0x00000146DFE04FA0>
gc: collectable <dict 0x00000146DFAF4F40>
gc: collectable <dict 0x00000146DFDE1640>
当前垃圾回收列表:[]
object del, id:0x146dfe04fd0
object del, id:0x146dfe04fa0
显式执行垃圾回收, 回收引用: 4
显式执行后的回收列表:[<__main__.ClassA object at 0x00000146DFE04FD0>, <__main__.ClassA object at 0x00000146DFE04FA0>, {'t': <__main__.ClassA object at 0x00000146DFE04FA0>}, {'t': <__main__.ClassA object at 0x00000146DFE04FD0>}]
gc.garbage:
垃圾回收后的对象会放在gc.garbage
列表里面gc.collect() :
会返回不可达(未能回收)的对象数目,此时为 4 ,表示是c1 c2两个对象、及其循环引用的属性t
其他API
gc.set_debug(flags)
设置 gc 的 debug 日志,一般设置为 gc.DEBUG_LEAK
gc.collect(*args, **kwargs) -> int # real signature unknown
- 参数:如果没有参数,则运行完整收集,可选参数可以是指定要收集的代数的整数,默认传入 2
- 0: 代表检查 第1代 对象
- 1: 代表检查 第1、2代 对象
- ....
- 无参: 执行一个 full collection
- 代数编号无效: 引发
ValueError
错误
- 返回值:不可达对象的数量
2.2 gc模块自动垃圾回收
必须要 import gc
模块,并且 is_enable()=True
才会启动自动垃圾回收
该作用就是发现并处理不可达的垃圾对象,垃圾回收 = 垃圾检查 + 垃圾回收
在 Python 中,采用分代收集的方法,把对象分为三代
- 对象在创建的时候,放在一代中
- 如果在一次一代的垃圾检查中,该对象存活下来,就会被放到二代中
- 在一次二代的垃圾检查中,该对象存活下来,就会被放到三代中
因此 gc 模块里面会有一个长度为 3 的列表的计数器,可通过
gc.get_count()
获取当前自动执行垃圾回收的计数器,返回一个长度为 3 的元组
print(gc.get_count()) # (35, 6, 1)
c1 = ClassA()
c2 = ClassA()
print(gc.get_count()) # (37, 6, 1) 此时分配内存数目 +2
(35, 6, 1)
其中35 是指距离上一次一代垃圾检查,Python 分配内存的数目减去释放内存的数目,注意是内存分配,而不是引用计数的增加 ,每位对应每一代的垃圾检查内存分配的次数
自动垃圾回收阀值
gc.get_threshold()
函数说明: 获取到的长度为 3 的元组,即 gc 模块获取到自动回收的频率,如: (700,10,10)
表示当前阀值
print(gc.get_threshold()) # (700, 10, 10)
gc.set_threshold(700, 3, 0)
print(gc.get_threshold()) # (700, 3, 0)
### 输出结果
(700, 10, 10)
(700, 3, 0)
关于 (700, 10, 10)
阈值和计数规则
- 当计数器从
(699,3,0)
增加到(700,3,0)
时,gc 模块就会执行gc.collect(0)
, 即检查一代对象的垃圾,并重置计数器为(0,4,0)
- 当计数器从
(699,9,0)
增加到(700,9,0)
,gc 模块就会执行gc.collect(1)
即检查一、二代对象的垃圾,并重置计数器为(0,0,1)
- 当计数器从
(699,9,9)
增加到(700,9,9)
,gc 模块就会执行gc.collect(2)
即检查一、二、三代对象的垃圾,并重置计数器为(0,0,0)
set_threshold(threshold0, threshold1=None, threshold2=None)
**函数说明:**设置垃圾回收的阈值。将 threshold0 设置为 0 将禁用垃圾回收
参数:
- threshold: int 设置第一代垃圾回收的阈值,若设置为 0,则禁止第一代垃圾回收
- threshold1: int 设置第二代垃圾回收的阈值,若未指定则使用默认值
- threshold2: int 设置第三代垃圾回收的阈值,若未指定则使用默认值
**返回值:**无
解释: 这个函数用于设置 Python 中自动垃圾回收的阈值。Python 自带的垃圾回收器会在对象数量达到一定程度时自动启动,清除不再被引用的对象。通过设置阈值,我们可以控制垃圾回收的时机和频率。其中,第一代垃圾回收是最频繁的,第三代垃圾回收是最少的。将某一代的阈值设置为 0 就可以禁用该代的垃圾回收
注意: 如果循环引用中,两个对象都定义了
__del__()
方法,gc 模块不会销毁这些不可达对象,因为 gc 模块不知道应该先调用哪个对象的__del__()
,故安全起见,gc 会把对象放到gc.garbage
中,但是不会销毁对象,因此出现上文所示的打印
总结:
项目中避免循环引用
引入 gc 模块,启动 gc 模块的自动清理循环引用的对象机制
由于分代收集,所以把需要长期使用的变量集中管理,并尽快移到二代以后,减少 GC 检查时的消耗
gc 模块唯一处理不了的是循环引用的类都有
__del__()
,故项目中要避免定义__del__()
方法,如果一定要使用该方法,同时导致循环引用,需要代码显式调用gc.collect()
将gc.garbage
里面的对象的显式回收调启动 gc 模块的自动清理功能会带来一些性能上的损失,因此在需要关注性能的场景下需要谨慎使用该功能
2. 弱引用
场景: 经常会使用 cache dict 这类功能,需要将一些信息存入一个全局的缓存字典中,如下
players = {}
class Player:
# 玩家类,若该玩家ID,不在缓存字典,则写入全局缓存
def __init__(self):
for i in range(1000):
if i not in players:
self.id = i
break
players[self.id] = self
def game():
""" 游戏初始化两个玩家 """
player1 = Player()
player2 = Player()
# p1 and p2 do something
for _ in range(10):
game()
print(len(players))
### 输出结果:
# 20
此时发现有 20 个缓存的玩家,尽管游戏结束,也一直存放于全局中未回收
当游戏源源不断进行,随着时间增长,缓存中的对象也会随之增长
2.1 weakref 弱引用
如果 player 实例 已经没有任何游戏的部分引用它了,即object 身上其他的引用都没有了,就可以触发引用垃圾回收被 release 掉了
weakref.WeakValueDictionary()
通常对字典的值使用 WeakValueDictionary()
,和字典的使用一致,但 对每个字典的值是弱引用,当该值上的其他引用归零时,会将值和key一起扔掉 ,若打印时,则需要将其转为 dict
import weakref
players = weakref.WeakValueDictionary()
class Player:
# 玩家类,若该玩家ID,不在缓存字典,则写入全局缓存
def __init__(self):
for i in range(1000):
if i not in players:
self.id = i
break
players[self.id] = self
def game():
""" 游戏初始化两个玩家 """
player1 = Player()
player2 = Player()
# p1 and p2 do something
for _ in range(10):
game()
print(len(players), dict(players))
### 输出结果:
# 0 {}
weakref.WeakKeyDictionary()
对字典的键进行弱引用,但对于 int、str 等 object,无法进行弱引用
有不少的 C level 的 builtin object 都是不支持弱引用的,对其(如:一个整数 )做弱引用也没有任何意义的
基本上,在 Python level 定义的 object 都是支持弱引用的,每个 instance 实例的方法、集合、生成器等都是可以被弱引用的
__slots__
2.2 关于 若 class 中定义了 __slots__ = ["id"]
,若还想让其支持弱引用,需要增加 "__weakref__"
这一项,它会保存弱引用相关的一些信息
class Player:
class Player:
__slots__ = ["id", "__weakref__"]