Python垃圾回收机制

时间:2021-09-17 06:11:17

垃圾回收机制

分为下三点:引用计数

      标记-清除

      分代回收

引用计数(b,w)(主要部分):

每内存中的数据与变量进行一次绑定,那么引用计数就会+1

如果引用计数小于1,(没有变量与数据进行绑定),那么就会被自动回收

 


 

引用计数(23)+1的情况:

创建对象:a = 23

对象被引用: b = a

对象func(a)

对象作为一个元素,存储在容器中,例如list1=[a,a]

引用计数(23) -1的情况:

对象的别名被销毁 del a

一个对象离开它的作用域,例如f函数执行完毕时,func函数中的局部变量(全局变量不会)

对象所在的容器被销毁,或从容器中删除对象

 


 

缺点:循环引用导致内存泄露

 

li1 = [11,22]

li2 = [22,33]

li1.append(li2)

li2.append(li1)

del li1

del li2

 

创建了li1,li2后,这两个对象的引用计数都是1,执行li1.append(li2)和li2.append(li1)后,引用计数变成2.

在del li1后,内存li1的对象的引用计数变为1,由于不是为0,所以li1的对象不会被销毁,同理,在del li2后也是一样的。

虽然它们两个的对象都可以被销毁,但是由于循环引用,所有垃圾回收器都不会回收它们,最终出现内存泄露。

 

于是就出现了标记-清除

 


 

标记-清除(w):

它分为两个阶段:第一阶段是标记阶段,GC会把所有的『活动对象』打上标记,第二阶段是把那些没有标记的对象『非活动对象』进行回收。

 

对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。

在上图中,我们把小黑圈视为全局变量,也就是把它作为root object,从小黑圈出发,对象1可直达,那么它将被标记,对象2、3可间接到达也会被标记,而4和5不可达,那么1、2、3就是活动对象,4和5是非活动对象会被GC回收。

标记清除算法作为Python的辅助垃圾收集技术主要处理的是一些容器对象,比如list、dict、tuple,instance等,因为对于字符串、数值对象是不可能造成循环引用问题。Python使用一个双向链表将这些容器对象组织起来。不过,这种简单粗暴的标记清除算法也有明显的缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。

 

 

Python垃圾回收机制

 

 

 

 

在上图中,假如红色的圈是全局变量,从红色的出发,对象3可以直达,那么它会被标记,对象1,2可以间接到达,也会被标记,然而对象4,5不可达;那么1,2,3是活动对象,4,5是非活动对象,4,5就会被回收

 

缺点:清楚非活动对象前它必须要去扫描整个堆内存,哪怕只剩下小部分活动对象也要烧苗所有的对象

 

 


 

分代回收(w):

由于“复制”算法对于存活时间长,大容量的储存对象需要耗费更多的移动时间,和存在储存对象的存活时间的差异。需要程序将所拥有的内存空间分成若干分区,并标记为年轻代空间和年老代空间。程序运行所需的存储对象会先存放在年轻代分区,年轻代分区会较为频密进行较为激进垃圾回收行为,每次回收完成幸存的存储对象内的寿命计数器加一。当年轻代分区存储对象的寿命计数器达到一定阈值或存储对象的占用空间超过一定阈值时,则被移动到年老代空间,年老代空间会较少运行垃圾回收行为。一般情况下,还有永久代的空间,用于涉及程序整个运行生命周期的对象存储,例如运行代码、数据常量等,该空间通常不进行垃圾回收的操作。

通过分代,存活在局限域,小容量,寿命短的存储对象会被快速回收;存活在全局域,大容量,寿命长的存储对象就较少被回收行为处理干扰。