垃圾回收机制应该是面试最常问的问题了, 那么 Python 中的垃圾回收机制 (Garbage Collection) 是怎么解决的呢? 我记得每一本 python 入门的书籍都会说 python 中请不要担心内存泄漏这个 问题, 那么这个背后又是什么原理, 今天就来 818.
Python 中的 GC 算法
分为下三点: 引用计数 / 标记 - 清除 / 分代回收
. 引用计数(主要)
刚开始学习 Python 的时候总是会有人告诉你, 万物皆对象是一大特色. 在 Python 中每一个对象的核心就是一个结构体 PyObject, 它的内部有一个引用计数器(ob_refcnt).
- typedef struct_object {
- int ob_refcnt;
- struct_typeobject *ob_type;
- } PyObject;
- a=10
引用计数的意思就是, 一个对象在它刚被 New 出来呱呱 (gugu 不是 guagua) 坠地的时候因为被 New 方法引用了所以他的引用计数就是 1, 如果它被引用 (也就是在之前的基础上 例如: b=a, 被丢入函数列表等等被引用就会在引用计数上加 1), 如果引用它的对象被删除的时候(在之前的基础上 DEL b) 那么它的引用计数就会减少一一直到当它的引用计数变为 0 的时候, 垃圾回收机制就会找上门做掉它(回收), 脑补一下 : 开门我是查水表的.
优点 / 缺点:
因为引用计数是 GC 主要方法, 来看一下优缺点.
优:
简单, 实时性(一旦为零就不跟你多 BB, 做掉)
缺:
. 维护性高(简单实时, 但是额外占用了一部分资源, 虽然逻辑简单, 但是麻烦. 好比你吃草莓, 吃一次洗一下手, 而不是吃完洗手.)
. 不能解决的情况:--->循环引用(如下):
- a=[1,2]
- b=[2,3]
- a.append(b)
- b.append(a)
- DEL a
- DEL b
说实话感觉还有点像死锁的问题, 这种问题出现在可以循环的结构中 List Dict Object 等等, 如上代码 a,b 间的引用都为 1, 而 a,b 被引用的对象删除后都各自减去 1(所以他们各自的引用计数还是 1), 这时候就尴尬了啊, 都是 1 就有了免死金牌(一直是 1 不会变化了). 这样的情况单单靠引用计数就无法解决了. 也为我们引入了下面的主题 标记 - 清除
. 标记 - 清除: 标记清除就是用来解决循环引用的问题的只有容器对象才会出现引用循环, 比如列表, 字典, 类, 元组. 首先, 为了追踪容器对象, 需要每个容器对象维护两个额外的指针, 用来将容器对象组成一个链表, 指针分别指向前后两个容器对象, 方便插入和删除操作. 试想一下, 现在有两种情况:
- A:
- a=[1,3]
- b=[2,4]
- a.append(b)
- b.append(a)
- del a
- del b
- B:
- a=[1,3]
- b=[2,4]
- a.append(b)
- b.append(a)
- del a
Okay, 现在开始说正题. 在标记 - 清除算法中, 有两个集中营, 一个是 root 链表(root object), 另外一个是 unreachable 链表.
. 对于情景 A, 原来再未执行 DEL 语句的时候, a,b 的引用计数都为 2(init+append=2), 但是在 DEL 执行完以后, a,b 引用次数互相减 1.a,b 陷入循环引用的圈子中, 然后标记 - 清除算法开始出来做事, 找到其中一端 a, 开始拆这个 a,b 的引用环(我们从 A 出发, 因为它有一个对 B 的引用, 则将 B 的引用计数减 1; 然后顺着引用达到 B, 因为 B 有一个对 A 的引用, 同样将 A 的引用减 1, 这样, 就完成了循环引用对象间环摘除.), 去掉以后发现, a,b 循环引用变为了 0, 所以 a,b 就被处理到 unreachable 链表中直接被做掉.
. 对于情景 B, 简单一看那 b 取环后引用计数还为 1, 但是 a 取环, 就为 0 了. 这个时候 a 已经进入 unreachable 链表中, 已经被判为死刑了, 但是这个时候, root 链表中有 b. 如果 a 被做掉, 那世界上还有什么正义... , 在 root 链表中的 b 会被进行引用检测引用了 a, 如果 a 被做掉了, 那么 b 就... 凉凉, 一审完事, 二审 a 无罪, 所以被拉到了 root 链表中.
QA: 为什么要搞这两个链表
之所以要剖成两个链表, 是基于这样的一种考虑: 现在的 unreachable 可能存在被 root 链表中的对象, 直接或间接引用的对象, 这些对象是不能被回收的, 一旦在标记的过程中, 发现这样的对象, 就将其从 unreachable 链表中移到 root 链表中; 当完成标记后, unreachable 链表中剩下的所有对象就是名副其实的垃圾对象了, 接下来的垃圾回收只需限制在 unreachable 链表中即可.
分代回收:
了解分类回收, 首先要了解一下, GC 的阈值, 所谓阈值就是一个临界点的值. 随着你的程序运行, Python 解释器保持对新创建的对象, 以及因为引用计数为零而被释放掉的对象的追踪. 从理论上说, 创建 == 释放数量应该是这样子. 但是如果存在循环引用的话, 肯定是创建>释放数量, 当创建数与释放数量的差值达到规定的阈值的时候, 当当当当~ 分代回收机制就登场啦.
垃圾回收 = 垃圾检测 + 释放.
分代回收思想将对象分为三代 (generation 0,1,2),0 代表幼年对象, 1 代表青年对象, 2 代表老年对象. 根据弱代假说(越年轻的对象越容易死掉, 老的对象通常会存活更久.) 新生的对象被放入 0 代, 如果该对象在第 0 代的一次 gc 垃圾回收中活了下来, 那么它就被放到第 1 代里面(它就升级了). 如果第 1 代里面的对象在第 1 代的一次 gc 垃圾回收中活了下来, 它就被放到第 2 代里面. gc.set_threshold(threshold0[,threshold1[,threshold2]]) 设置 gc 每一代垃圾回收所触发的阈值. 从上一次第 0 代 gc 后, 如果分配对象的个数减去释放对象的个数大于 threshold0, 那么就会对第 0 代中的对象进行 gc 垃圾回收检查. 从上一次第 1 代 gc 后, 如过第 0 代被 gc 垃圾回收的次数大于 threshold1, 那么就会对第 1 代中的对象进行 gc 垃圾回收检查. 同样, 从上一次第 2 代 gc 后, 如过第 1 代被 gc 垃圾回收的次数大于 threshold2, 那么就会对第 2 代中的对象进行 gc 垃圾回收检查.
这算是简单的讲完了 Python 的 GC, 打完收工.
来源: https://juejin.im/post/5b34b117f265da59a50b2fbe