Python 垃圾回收机制

Python 垃圾回收(Garbage Collection)

Python 垃圾回收机制主要为 引用计数标记-清除分代收集 辅助

引用计数

在每次分配和释放内存的时候, 加入管理引用计数的动作
当引用计数器为零时,该内存就会被销毁

特点

优点:简单、高效、实时(将处理垃圾时间分摊到运行代码时,而不是等到一次回收)
缺点:无法解决循环引用的问题

工作原理

引用计数 + 1

进行以下操作时,引用计数会 + 1:

  1. 创建对象
  2. 引用对象

引用计数 - 1

进行以下操作时,引用次数 - 1:

  1. 被销毁
  2. 指向其他对象

查看引用次数

可以使用 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)

标记-清除算法分为两个阶段:

  1. 标记活动对象
  2. 回收非活动对象

有两个链表,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 阈值

如果不存在循环引用,则创建的对象最后都会被回收,也就是创建对象的数量等于释放的数量
但是出现循环引用就会使得创建的数量大于释放的,一旦这个差值超过一定数量,就会触发分代回收机制

回收触发时机

  1. 调用 gc.collect()
  2. 到达阈值
  3. 退出程序

posted on 2019-11-25 15:30  doubtful  阅读(678)  评论(0编辑  收藏  举报

导航