「python」垃圾回收

python垃圾回收机制分析

Posted by odin on April 13, 2020

python的gc由python虚拟机自动帮我们实现,GC彻底把程序员从资源管理的重担中解放出来,让他们有更多的时间放在业务逻辑上。但这并不意味着码农就可以不去了解GC,毕竟多了解GC知识还是有利于我们写出更健壮的代码。我觉得还是有必要对python垃圾回收机制有一个比较清楚的认识。 gc专注于两件事:

  1. 找到内存中无用的垃圾资源。
  2. 清除这些垃圾并把内存让出来给其他对象使用。

python的gc机制

1.引用计数

python的主要也是默认的gc回收机制,简单的说就是每一个对象添加一个计数器,有被引用时计数器+1,引用失效计数器-1。一旦对象的引用计数为0,该对象立即被回收,对象占用的内存空间将被释放。

python一切皆对象,他们的核心是一个PyObject结构体:

typedef struct_object {
 int ob_refcnt;
 struct_typeobject *ob_type;
} PyObject;

PyObject每个对象都有,其中ob_refcnt就是作为引用计数器存在。当有新的引用时ob_refcnt加1,引用删除时ob_refcnt减1,当ob_refcnt==0时该对象生命周期结束,被gc回收。

引用计数机制优点:

  1. 简单。
  2. 实时:一旦没有引用,内存就直接释放了。不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时。

同时引用计数也有他的缺点:

  1. 资源消耗:需要额外的维护计数器的资源开销。
  2. 无法处理循环引用的情况。

    循环引用:

    1
    2
    3
    4
    5
    6
    
      a = { }     #对象a的引用计数为 1
      b = { }     #对象b的引用计数为 1
      a['b'] = b  #b的引用计数增1
      b['a'] = a  #a的引用计数增1
      del a       #手动a的引用减 1,最后a对象的引用为 1
      del b       #手动b的引用减 1, 最后b对象的引用为 1
    

    a和b相互引用而再没有外部引用a与b中的任何一个,它们的引用计数虽然都为1,但显然应该被回收。循环引用是引用计数机制中最大的缺陷,这也是有些语言gc没有采用引用计数机制的原因,例如java。

2.标记清除

标记清除(Mark—Sweep)算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。分为两个阶段:

  1. gc给活动的对象打标记。
  2. gc把没有标记的对象进行回收。 对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。 在上图中,我们把小黑圈视为全局变量,也就是把它作为root object,从小黑圈出发,对象1可直达,那么它将被标记,对象2、3可间接到达也会被标记,而4和5不可达,那么1、2、3就是活动对象,4和5是非活动对象会被GC回收。

标记清除算法作为Python的辅助垃圾收集技术主要处理的是一些容器对象,比如list、dict、tuple,instance等,因为对于字符串、数值对象是不可能造成循环引用问题。Python使用一个双向链表将这些容器对象组织起来。这种简单粗暴的标记清除算法也有明显的缺点:

  1. 清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。

3.分代回收

分代回收是一种以空间换时间的操作方式,Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。同时,分代回收是建立在标记清除技术基础之上。分代回收同样作为Python的辅助垃圾收集技术处理那些容器对象