Python 垃圾回收(Garbage Collection)
Python 垃圾回收机制主要为 引用计数
,标记-清除
和 分代收集
辅助
引用计数
在每次分配和释放内存的时候, 加入管理引用计数的动作
当引用计数器为零时,该内存就会被销毁
特点
优点:简单、高效、实时(将处理垃圾时间分摊到运行代码时,而不是等到一次回收)
缺点:无法解决循环引用的问题
工作原理
引用计数 + 1
进行以下操作时,引用计数会 + 1:
- 创建对象
- 引用对象
引用计数 - 1
进行以下操作时,引用次数 - 1:
- 被销毁
- 指向其他对象
查看引用次数
可以使用 sys.getrefcount
函数查看引用次数
示例:
from sys import getrefcount
a = 'cnblogs.com/dbf-/'
print(getrefcount(a))
b = a
print(getrefcount(a))
del b
print(getrefcount(a))
输出结果:
4
5
4
循环引用
string、number 对象并不会产生循环引用,但是 list、dict 等内部可以引用其他对象的就会产生
示例:
a = list()
b = dict()
a.append(b)
b['a'] = a
del a
del b
外部并没有引用 a、b 两个对象,但是它们的应用次数却同时加一,即使将 a、b 删除引用次数依然不为 0
所以无法通过引用计数发现并清除
标记-清除 (Mark-weep)
标记-清除算法分为两个阶段:
- 标记活动对象
- 回收非活动对象
有两个链表,root 和 unreachable
root 为全局变量,unreachable 为非活动对象
将非活动对象添加到 unreachable 链表中,之后进行删除
特点
清除对象前需要扫描整个堆内存
工作原理
从 root 链表出发,按照引用方向进行遍历,并将引用次数减一,如果减到 0 且没有被其他对象引用,则
回收示例:
a = list()
b = list()
a.append(b)
b.append(a)
del a
del b
此时 a、b 的引用计数均不为 0,所以不会被引用计数机制回收
开始标记时,首先找到 a,因为 a 中有对 b 的引用,所以将 b 的引用次数减一,之后通过引用到达 b,由于 b 中有对 a 的引用,所以将 a 的引用次数减一
这时 a、b 引用次数均为 0,所以被添加到 unreachable 链表中清除
不回收示例:
a = list()
b = list()
a.append(b)
b.append(a)
del a
此时 a、b 的引用计数均不为 0,所以不会被引用计数机制回收
开始标记时,首先找到 a,因为 a 中有对 b 的引用,所以将 b 的引用次数减一,之后通过引用到达 b,由于 b 中有对 a 的引用,所以将 a 的引用次数减一
这时 a 引用次数均为 0,被添加到 unreachable 链表;由于 b 不为 0 且引用 a,所以 a 被从 unreachable 链表中取出
分代收集
Python 将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python 将内存分为了 3 代,分别为 零代、一代、二代(generation 0, 1, 2)
他们对应的是 3 个链表,它们的垃圾收集频率与对象的存活时间的增大而减小
年轻代链表的总数达到上限时,Python 垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推
老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内
同时,分代回收是建立在标记清除技术基础之上,分代回收同样作为 Python 的辅助垃圾收集技术处理那些容器对象
弱代假说
年轻的对象更容易死掉,年老的对象容易存在更长的时间
因此将新创建的对象放入零代链表,零代进行扫描的频率更高,扫描后依然存活的对象放入高一级链表,越高级的链表扫描频率越低
零代链表
每当创建一个对象时,都会将它加入零代链表
零代链表中都是最年轻的对象
GC 阈值
如果不存在循环引用,则创建的对象最后都会被回收,也就是创建对象的数量等于释放的数量
但是出现循环引用就会使得创建的数量大于释放的,一旦这个差值超过一定数量,就会触发分代回收机制
回收触发时机
- 调用
gc.collect()
- 到达阈值
- 退出程序