这个问题也是目前面试中经常被问到的问题了,因此这里想要详细写一下记录一下。这次从以下三点:引用计数、标记-清除、分代回收来详细的介绍一下。
一、引用计数
引用计数是GC
主要方法,意思就是一个对象在它刚被New
出来的时候因为被New
方法引用了所以他的引用计数就是1,如果它被引用(也就是在之前的基础上,例如:b=a
,被丢入函数列表等等被引用就会在引用计数上加1),如果引用它的对象被删除的时候(在之前的基础上DEL b
)那么它的引用计数就会减少一一直到当它的引用计数变为0的时候,垃圾回收机制就会找上门做掉(回收)
那么接下来看一下引用计数方法的优缺点。
优点:
- 简单,实时性(一旦为零就不跟你多BB,做掉)
- 运行期没有停顿 可以类比一下Ruby的垃圾回收机制,也就是
实时性
一旦没有引用,内存就直接释放了。不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时。 - 对象有确定的生命周期
- 易于实现
缺点:
- 维护引用计数消耗资源,维护引用计数的次数和引用赋值成正比。
- 无法解决循环引用的问题。A和B相互引用而再没有外部引用A与B中的任何一个,它们的引用计数都为1,但显然应该被回收。
循环引用的示例:
list1 = []
list2 = []
list1.append(list2)
list2.append(list1)
引用计数无法解决此类问题,因此需要引入新的机制。
二、 标记-清除
标记清除就是用来解决循环引用的问题。
只有容器对象才会出现引用循环,比如列表、字典、类、元组。 首先,为了追踪容器对象,需要每个容器对象维护两个额外的指针, 用来将容器对象组成一个链表,指针分别指向前后两个容器对象,方便插入和删除操作。
在标记-清除算法中,有两个集中营,一个是root
链表(root object)
,另外一个是unreachable
链表。
QA: 为什么要搞这两个链表
之所以要剖成两个链表,是基于这样的一种考虑:现在的unreachable
可能存在被root
链表中的对象,直接或间接引用的对象,这些对象是不能被回收的,一旦在标记的过程中,发现这样的对象,就将其从unreachable
链表中移到root
链表中;当完成标记后,unreachable
链表中剩下的所有对象就是名副其实的垃圾对象了,接下来的垃圾回收只需限制在unreachable
链表中即可。
简单来说就是对已标记的对象进行重新筛查,随后真正删除。
标记清除(Mark—Sweep)』算法是一种基于追踪回收(
tracing GC
)技术实现的垃圾回收算法。它分为两个阶段:第一阶段是标记阶段,GC会把所有的『活动对象』打上标记,第二阶段是把那些没有标记的对象『非活动对象』进行回收。
那么GC又是如何判断哪些是活动对象哪些是非活动对象的呢?
对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。
在上图中,我们把小黑圈视为全局变量,也就是把它作为root object,从小黑圈出发,对象1可直达,那么它将被标记,对象2、3可间接到达也会被标记,而4和5不可达,那么1、2、3就是活动对象,4和5是非活动对象会被GC回收。
三、分代回收
说到分代回收,就不得不提到gc的阈值和逻辑。
GC
的阈值,所谓阈值就是一个临界点的值。
随着你的程序运行,Python解释器保持对新创建的对象,以及因为引用计数为零而被释放掉的对象的追踪。
下面是GC
的逻辑:
1. -> 1、分配内存
2. -> 发现超过阈值了
3. -> 触发垃圾回收
4. -> 将所有可收集对象链表放到一起
5. -> 遍历, 计算有效引用计数
6. -> 分成 有效引用计数=0 和 有效引用计数 > 0 两个集合
7. -> 大于0的, 放入到更老一代
8. -> =0的, 执行回收
9. -> 回收遍历容器内的各个元素, 减掉对应元素引用计数(破掉循环引用)
10. -> 执行-1的逻辑, 若发现对象引用计数=0, 触发内存回收
11. -> 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将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了三代,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。
一个代,Python将内存分为了三代,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。