• 垃圾回收机制


    一、什么是Python垃圾回收机制

      垃圾回收机制(简称GC)是Python解释器自带一种机制,专门用来回收不可用的变量值所占用的内存空间。每种语言都会有其特有的垃圾回收机制,以实现对计算机内存资源的最大利用。

    二、为什么要有Python垃圾回收机制

      因为Python程序在运行中会申请大量的内存空间(堆区),而当Python不再能够访问到这些内存空间时,这些内存空间就成了垃圾。若不对这些垃圾进行处理的话,随着程序的运行,内存就会被大量的垃圾所占据,最后导致运行缓慢,甚至宕机。不同于别的语言,Python解释器自带的垃圾回收机制,替程序员完成了堆区内存的申请与释放。

    三、什么是Python垃圾回收机制

    3.1 Python中变量存放规则

      在Python中定义变量时,变量名和变量值都是需要存储的,分别对应内存中的两块区域:栈区与堆区。

    PS:在C中概念
    
    栈区:由系统自动分配释放,存放函数内的局部变量,形参和函数返回值。不需要开发人员手动管理。
    堆区:由程序员调用malloc()来主动申请,使用free()来释放内存。若申请了堆区内存,之后忘记释放内存,很容易造成内存泄漏。若程序员不释放,在程序结束时,由操作系统回收。
    

    经过这一段时间的学习,按照我的理解,Python中的变量都是在栈区申请的指针类型变量,并指向堆区中,Python解释器给该变量开辟的对应内存。其结构如下图(转自Egon老师的知乎):

    x = 10
    y = 20
    

    img

    当我们执行如下代码时候,将发生变化。x将在栈区重新申请一个空间来拷贝y变量存的内存地址(我认为是,在底层C语言中,x不知道收到的指针是什么类型,于是根据y的指针类型在栈区重新开辟空间):

    x = y
    

    img

    3.2 直接引用和间接引用

      直接引用是从栈区出发,直接引用到的内存地址。(栈—>堆)

      间接引用是从栈区出发引用到堆区后,再通过进一步引用才能到达的内存地址。(堆—>堆)

    如(转自Egon老师的知乎):

    l2 = [20, 30]  # 列表本身被变量名l2直接引用,包含的元素被列表间接引用
    x = 10  # 值10被变量名x直接引用
    l1 = [x, l2]  # 列表本身被变量名l1直接引用,包含的元素被列表间接引用
    

    img

    四、垃圾回收机制的原理

    主要机制包含以下三块:

    1、引用计数:根据变量值被变量名关联的次数来跟踪和回收垃圾。(最主要)

    2、标记-清除:解决出现循环引用,已无法访问的内存不能被释放问题。(优化)

    3、分代回收:以空间换取时间的方法提高垃圾回收机制的效率。(优化)

    4.1引用计数

    age = 18		#此时值18的引用计数为1
    m = age			#此时值18的引用计数变为2
    

    如下(转自Egon老师的知乎):

    img

    img

    引用计数的减少:

    age = 10	#age与值18解除关联,再与10建立关联。此时值18的引用计数变为1
    del m		#del 即使变量名m与值18解除关联,此时值18的引用计数变为0
    

    此时值18的引用计数为0,根据Python垃圾回收机制(引用计数规则),值18所占用的内存空间应被解释器回收。

    5.2 标记-清除 机制

    问题:

    引用计数机制虽然可以收回大部分情况下的垃圾,但是也有其缺陷。有一种情况,即循环引用。

    # 如下我们定义了两个列表,简称列表1与列表2,变量名l1指向列表1,变量名l2指向列表2
    >>> l1=['xxx']  # 列表1被引用一次,列表1的引用计数变为1   
    >>> l2=['yyy']  # 列表2被引用一次,列表2的引用计数变为1   
    >>> l1.append(l2)             # 把列表2追加到l1中作为第二个元素,列表2的引用计数变为2
    >>> l2.append(l1)             # 把列表1追加到l2中作为第二个元素,列表1的引用计数变为2
    

    preview

    循环引用会导致:值不再被任何栈区的变量名关联,但是值的引用计数不是0,此时无法根据第一种引用计数机制回收。操作如下:

    >>> del l1 # 列表1的引用计数减1,列表1的引用计数变为1
    >>> del l2 # 列表2的引用计数减1,列表2的引用计数变为1
    

    img

    此时两个列表的引用计数仍为1,但是两个列表不再被任何其他对象关联,我们是无法访问到这两个列表的。此时这两个列表无疑是垃圾,然而却无法被回收。一旦程序运行到一定程度,这种内存占用最终会导致程序的崩溃。

    解决方案:

    容器对象(比如:list,set,dict,class,instance)都可以包含对其他对象的引用,所以都可能产生循环引用。而“标记-清除”计数就是为了解决循环引用的问题。

    标记/清除算法的做法是当应用程序可用的内存空间被耗尽的时,就会停止整个程序,然后进行两项工作,第一项则是标记,第二项则是清除

    #1、标记
    通俗地讲就是:
    栈区相当于“根”,凡是从根出发可以访达(直接或间接引用)的,均可存活。
    
    具体地:标记的过程其实就是,遍历所有的GC Roots对象(以栈区中的所有内容或者线程作为顶点,类似使用prim算法,遍历整个堆区顶点(一种图结构)),然后将所有GC Roots的对象可以直接或间接访问到的对象标记为存活的对象,其余的均为非存活对象,应该被清除。
    
    #2、清除
    清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。
    

    img

    5.3分代回收机制

    问题:

    由于基于引用计数的回收机制,每次回收内存,都需要把所有对象的引用计数都遍历一遍,这是非常消耗时间的,于是引入了分代回收来提高回收效率,分代回收采用的是用“空间换时间”的策略。

    解决方案:

    分代指的是根据存活时间来为变量划分不同等级(也就是不同的代)

    新定义的变量,放到新生代这个等级中,假设每隔1分钟扫描新生代一次,如果发现变量依然被引用,那么该对象的权重(权重本质就是个整数)加一,当变量的权重大于某个设定得值(假设为3),会将它移动到更高一级的青春代,青春代的gc扫描的频率低于新生代(扫描时间间隔更长),假设5分钟扫描青春代一次,这样每次gc需要扫描的变量的总个数就变少了,节省了扫描的总时间,接下来,青春代中的对象,也会以同样的方式被移动到老年代中。也就是等级(代)越高,被垃圾回收机制扫描的频率越低

    img

    这种方法无疑可以提升效率,不过当一个新生代变量又或者一个青春代变量,在扫描后晋升,但立即便解除了绑定关系,这时就会有一定的延迟。不过综合来看,这种做法仍能大幅度提升效率,瑕不掩瑜。

  • 相关阅读:
    [ZJOI2008]树的统计 树链剖分
    CF915E 动态开线段树
    Poj 2114 Boatherds(点分治)
    Poj 2599 Godfather(树的重心)
    Bzoj 2152: 聪聪可可(点分治)
    Cogs 1714. [POJ1741][男人八题]树上的点对(点分治)
    Cogs 329. K- 联赛(最大流)
    Cogs 731. [网络流24题] 最长递增子序列(最大流)
    Bzoj 2282: [Sdoi2011]消防(二分答案)
    Cogs 732. [网络流24题] 试题库(二分图)
  • 原文地址:https://www.cnblogs.com/zhubincheng/p/12422772.html
Copyright © 2020-2023  润新知