2.python基础语法--垃圾回收

1.内存管理:

  ▪ python中的变量无需事先声明,且不需要指定类型。

  ▪ python无需关心变量使用前后是否存在的问题,无需关心其对内存的占用情况。

  ▪ python的垃圾回收机制,当python认为某个对象无用的时候,会自动的回收。

2.python的垃圾回收机制(GC):

  python中的每一个东西都是对象,对于垃圾回收,python采用的是引用计数器制为主,标记-清除和分代收集两种机制为辅的策略。

  引用计数法:

  引用计数法的原理是每个对象维护一个ob_ref,用来记录当前对象被引用的次数,也就是来追踪到底有多少引用指向了这个对象。

  当引用计数器为0时,那么该对象所占用的内存会被python虚拟机销毁。

  引用计数器+1的情况:

    ▪ 对象被创建时:a = 1

    ▪ 对象被引用时:b = a

    ▪ 对象被作为参数,传到函数时:def test(a)

    ▪ 对象被作为参数,存储在容器中:list = {a, "a", "b", 2}

  引用计数器-1的情况:

    ▪ 对象的别名被显式销毁:del a

    ▪ 对象的别名被赋予新的对象:a = 2

    ▪ 对象离开其作用域:例如test(a)函数执行完毕时,函数里面的局部变量的引用计数器就会减一(但是全局变量不会)

      ▪ 将该元素从容器中删除,或者容器被销毁。

  引用计数法维护引用计数消耗资源,且无法解决循环引用的问题。何为循环引用的问题?A和B相互引用而再没有外部引用A与B中的任何一个,它们的引用计数都为1,但显然应该被回收。(list1 = [],list2 = [],list1.append(list2),list2.append(list1),因为list1与list2都不符合增加计数的情况,因此其计数都为1,但是它们又互相循环引用。)

  为了解决这两个缺点,python又使用了标记-清除和分代回收这两种GC机制。

  标记-清除:

  也许你的代码会在不经意间陷入循环引用的情况,且这些对象没有外部引用,或者说程序已经不需要这些对象,那么python如何释放这些对象并收回它们所占的内存空间。

  标记清除(Mark—Sweep)算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。它分为两个阶段:第一阶段是标记阶段,GC会把所有的“活动对象”打上标记,第二阶段是把那些没有标记的对象“非活动对象”进行回收。那么GC又是如何判断哪些是活动对象哪些是非活动对象的呢?对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。

                                                                        

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

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

  python会使用一种链表来持续追踪活跃的对象,称之为“零代链表”。每次当你创建一个对象或其他什么值的时候,Python会将其加入零代链表。标记-清除法是为了解决循环引用问题。可以包含其他对象引用的容器对象(如list, dict, set,甚至class)都可能产生循环引用,为此,在申请内存时,所有容器对象的头部又加上了PyGC_Head来实现“标记-清除”机制。任何一个python对象都分为两部分: PyObject_HEAD + 对象本身数据。

  Python会循环遍历零代列表上的每个对象,检查列表中每个互相引用的对象,根据规则减掉其引用计数在这个过程中,Python会一个接一个的统计内部引用的数量以防过早地释放对象。

  许多不同的对象都放在零代链表里面,这些对象中有的被链表之外的其他对象所引用,当零代链表中的某些对象的引用计数变为零,这意味着收集器可以释放它们并回收内存空间了。剩下的活跃的对象则被移动到一个新的链表:一代链表。

  随着程序的运行,Python解释器保持对新创建的对象,以及因为引用计数为零而被释放掉的对象的追踪。从理论上说,这两个值应该保持一致,因为程序新建的每个对象都应该最终被释放掉。(这里怎么理解呢?就是创建了n个对象,python解释器记录新创建的对象个数为n,而理论上这n个对象最终都会计数为零被释放掉,因此python解释器记录被释放掉的对象的个数也为n。)

  然而,因为循环引用的原因,并且因为程序使用了一些比其他对象存在时间更长的对象,从而被分配对象的计数值与被释放对象的计数值之间的差异在逐渐增长。一旦这个差异累计超过某个阈值,则Python的收集机制就启动了,释放“浮动的垃圾”,并且将剩下的对象移动到一代链表。

  随着时间的推移,程序所使用的对象逐渐从零代链表移动到一代链表。而Python对于一代链表中对象的处理遵循同样的方法,一旦被分配计数值与被释放计数值累计到达一定阈值,Python会将剩下的活跃对象移动到二代链表。

  通过这种方法,代码所长期使用的对象,那些你的代码持续访问的活跃对象,会从零代链表转移到一代再转移到二代。通过不同的阈值设置,Python可以在不同的时间间隔处理这些对象。Python处理零代最为频繁,其次是一代然后才是二代。

  分代回收:

  GC的逻辑过程如下:

  

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

声明:该文章中的多数内容来自https://blog.csdn.net/xiongchengluo1129/article/details/80462651

 

  

 

原文地址:https://www.cnblogs.com/linfengs/p/11543699.html