python(8): 垃圾回收:引用计数,标记清除,分代回收

    Python 程序在运行的时候,需要在内存中开辟出一块空间,用于存放运行时产生的临时变量;计算完成 后,再将结果输出到永久性存储器中。如果数据量过大,内 存空间管理不善就很容易出现 OOM(out of memory), 俗称爆内存,程序可能被操作系统中止。
    内存泄漏也不是指你的内存在物理上消失了,而是意味着 代码在分配了某段内存后,因为设计错误,失去了对这段 内存的控制,从而造成了内存的浪费。 对于 不会再用到的内存空间,Python 是通过什么机制来回收这 些空间的呢?

1.引用计数

当这 一个对象的引用计数(指针数)为 0 的时候,说明这个对象永
不可达,自然它也就成为了垃圾,需要被回收。
import os
import psutil

def show_memory_info(hint):
    pid = os.getpid()
    p = psutil.Process(pid)
    info = p.memory_full_info()
    memory = info.uss / 1024 / 1024
    print('==>',memory)

def func():
    show_memory_info('in func 0')
    a = [i for i in range(1000000)]
    show_memory_info('in func 1')

func()
show_memory_info('end')

====》
==> 7.3203125
==> 46.0546875
==> 7.53515625

以上可见:局部变量在函数func内部创建后,内存增加,

函数执行完,局部变量的引用会注销掉--a所指代的对象引用数为0,即内存被回收

若将啊改为全局变量则func函数被调用完后不会被回收

手动释放

先调用del a   ;  再调用gc.collect()即可手动启动垃圾回收

2.标记清除(mark-sweep)

举例:对于一个有向图,如果从一个节点出发遍历,并标记每个经过的节点,那么在遍历结束后,所有没有被标记的节点,我们就称之为不可达节点。不可达节点会被回收。

mark-sweep使用双向链表维护一个数据结构,并值考虑容器类对象。

这里,我们介绍一种自动管理堆内存的算法:Mark and Sweep Algorithm(标记清除算法)

Mark and Sweep Algorithm(标记清除算法)

一般而言,垃圾回收算法都会包含两个基本操作。

操作1,检测所有不可达对象;

步骤2,回收不可达对象所占用的堆内存。

Mark and Sweep Algorithm(标记清除算法)在下面两个阶段执行这两个操作:

1)Mark phase(标记阶段)

2)Sweep phase(清除阶段)

Mark phase(标记阶段)

在对象创建时,设置它的标记位为0(false)。在Mark phase(标记阶段),我们设置所有可达对象的标记位为1。这一设置过程可以通过图遍历算法来完成,比如深度优先遍历算法。我们可以将每个对象看作图的一个结点,从每一个可达的结点出发,访问和它相连的没有被访问过的结点,并将这些结点标记为可达,然后递归访问与这些结点相连的没有被访问过的结点。

伪代码如下:

  • root变量引用了一个可以被局部变量直接访问的对象。这里我们假设只有一个root变量。
  • markedBit(obj)用来访问一个对象的标记位。
Mark(root)
    If markedBit(root) = false then
        markedBit(root) = true
        For each v referenced by root
             Mark(v)

提示:如果有多个root变量,对每个root变量调用Mark()即可。

Sweep phase(清除阶段)

这一阶段将回收所有不可达对象所占用的堆内存。所有标记位为false的对象,会被清除,其余标记位为true的对象,会将其标记为设置为false。

伪代码如下:

Sweep()
	For each object p in heap
		If markedBit(p) = true then
			markedBit(p) = false
		else
			heap.release(p)

3.分代收集(generational)

python将所有对象分为三代。刚创立的对象为第0代;经过一次垃圾回收之后,依然存在的对象便会从上一代挪到下一代。而每一代启动自动垃圾回收的阈值可以单独指定。当垃圾回收器中新增对象减去删除对象达到相应的阈值时,就会对这一代对象启动垃圾回收。

分代收集思想是新生的对象更有可能被垃圾回收,而存活更久的对象有更高概率继续存活,从而减少计算量,提高python性能。

总结:

python运行时维护两张链表:

一是refchain双向链表,存储所有对象创建和引用计数,

另一个只维护可能存在循环引用的对象(容器对象)

python内部会在某些情况下扫描可能循环引用的链表的每个元素,如果存在循环引用,那么让双方的引用计数器都减1,减1后等于0则被垃圾回收

标记清除存在的问题:

1.什么情况下出发

2.多久扫描一次

3.可能存在的循环引用的链表比较大,耗时旧

正对以上问题使用分代回收:

可能存在循环引用的分为3代: 0 代,1代 ,2代。例:第0代对象达800个是启动扫描,0代扫描10则1代扫描1次

参考:

Python的内存管理和垃圾回收机制2:标记清除和分代回收 - 简书


版权声明:本文为qq_37674086原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。