python的垃圾回收是采用的引用计数算法,而且在引用计数的基础上辅以标记-清除和分代回收算法。以引用计数算法来跟踪和回收垃圾;以标记-清除来解决对象产生循环引用造成无法回收的问题;以分代回收以空间换时间来进一步提高垃圾回收!
我们从它的内存分配开始,说说它的垃圾回收机制!
内存分配器
在python中,当要分配内存空间时,不单纯使用malloc/free,而是在其基础上堆放3个独立的分层,有效率的进行分配。如下图所示。
第0层往下的是OS的功能。python中并不是在生成所有对象时都会调用malloc函数,而是根据要分配的内存大小来改变分配的方法。申请的内存大小如果小于256K字节,就老实地调用malloc();如果小于等于256K字节,就轮到第1层和第2层出场了。
当遇到小于等于256字节的对象时,第1层内存分配器就出场了!在这一层会事先从第0层开始迅速保留内存空间,将其蓄积起来,这一层的作用就是管理这部分蓄积的空间。它的内存结构分为arena、pool、block,其大小顺序是arena>pool>block。为了避免频繁调用malloc和free,第0层的分配器会以最大的单位arena来保留内存。pool是用于有效管理空的block的单位。arena的大小是256K字节;pool的大小一般是4K字节,也就是虚拟内存页的大小,注意,每个pool的开头地址都是4K字节的倍数。
第2层负责管理pool内的block,它会将block的开头地址返回给申请者,并释放block等。这个block的大小是8的倍数,(按8字节来对齐)相应的它的地址也是8的倍数。
第3层分配器是对象特有的分配器,对象有对象和元组等多种多样的型,在生成它们的时候要使用各自持有的分配器。
python的引用计数算法
在引用计数法中,各个对象的内部都有着计数器。如果对象的引用数量增加,就在计数器上加1,反过来如果引用计数数量减少,就在计数器上减去1。
那么那些情况加1呢?1、对象被创建,例如a =1;2、对象被引用,例如b=a;3、对象被作为参数传入到一个函数中,例如func(a);4、对象作为一个元素,存储在容器中,例如list=[a]。
哪些情况引用计数减1?1、对象的别名被显示销毁,例如del a;2、对象的别名被赋值给新的对象,例如a=10;3、一个对象离开它的作用域,例如f函数执行完毕时,func函数中的局部变量;4、对象所在的容器被销毁,或者从容器中删除对象。
import sys
def func(n1,*n2):
print('in func function:',sys.getrefcount(n1) - 1)
print('init',sys.getrefcount(1) - 1)
a = 1
print('after a =1',sys.getrefcount(1) - 1)
func(a)
print('after func(a)',sys.getrefcount(1) - 1)
list = [a,12,13]
print('after list',sys.getrefcount(1) - 1)
b = a
print('after b = a',sys.getrefcount(1) - 1)
a = 10
print('after a = 10',sys.getrefcount(1) - 1)
del a
print('after del a',sys.getrefcount(1) - 1)
del b
print('after del b',sys.getrefcount(1) - 1)
del list
print('after list',sys.getrefcount(1) - 1)
#init 129
#after a =1 130
#in func function: 132
#after func(a) 130
#after list 131
#after b = a 132
#after a = 10 131
#after del a 131
#after del b 130
#after list 129
注意,sys.getrefcount()查看对象的引用计数,因为其调用的时候会让对象加1,所以在后面有一个减1。
after func(a)之后的引用数和之前是一样的,因为当函数执行完之后,作为参数的引用会自动销毁,所以其引用数值不变。
当对象的引用计数减少为0时,就意味着对象没有被任何人使用了,这个时候就会将其所占用的内存释放了。这也是引用计数算法的一大优势,即“实时性”,任何内存,一旦没有指向它的引用,就会立即被回收。但是由于引用计数带来的额外操作,还有运行时内存的分配和释放,使其性能会降低。
最大的软肋是当出现相互引用,形成环的时候,引用计数就无能为力了。这个时候标记-清除与分代算法就登场了。
分代算法
因为在python运行的过程中,GC不能处理未使用的对象因为因为计数值不会到0.这个时候就出现了分代算法。python会使用一种链表来持续追踪活跃的对象,这个把这个链表称之为零代。每当我们创建一个对象时,python会将其加入零代链表。
随后,Python会循环遍历零代列表上的每个对象,检查列表中每个互相引用的对象,根据规则减掉其引用计数。在这个过程中,Python会一个接一个的统计内部引用的数量以防过早地释放对象。通过识别内部引用,Python能够减少许多零代链表对象的引用计数。当对象的引用计数变为零,这意味着收集器可以释放它们并回收内存空间了。剩下的活跃的对象则被移动到一个新的链表:一代链表。
Python中的GC阈值
Python什么时候会进行这个标记过程?随着你的程序运行,Python解释器保持对新创建的对象,以及因为引用计数为零而被释放掉的对象的追踪。从理论上说,这两个值应该保持一致,因为程序新建的每个对象都应该最终被释放掉。
当然,事实并非如此。因为循环引用的原因,并且因为你的程序使用了一些比其他对象存在时间更长的对象,从而被分配对象的计数值与被释放对象的计数值之间的差异在逐渐增长。一旦这个差异累计超过某个阈值,则Python的收集机制就启动了,并且触发上边所说到的零代算法,释放“浮动的垃圾”,并且将剩下的对象移动到一代列表。
随着时间的推移,程序所使用的对象逐渐从零代列表移动到一代列表。而Python对于一代列表中对象的处理遵循同样的方法,一旦被分配计数值与被释放计数值累计到达一定阈值,Python会将剩下的活跃对象移动到二代列表。
通过这种方法,你的代码所长期使用的对象,那些你的代码持续访问的活跃对象,会从零代链表转移到一代再转移到二代。通过不同的阈值设置,Python可以在不同的时间间隔处理这些对象。Python处理零代最为频繁,其次是一代然后才是二代。
标记-清除
标记-清除只关注那些可能会产生循环引用的对象,显然,像是PyIntObject、PyStringObject这些不可变对象是不可能产生循环引用的,因为它们内部不可能持有其它对象的引用。Python中的循环引用总是发生在container对象之间,也就是能够在内部持有其它对象的对象,比如list、dict、class等等。这也使得该方法带来的开销只依赖于container对象的的数量。
原理:1. 寻找跟对象(root object)的集合作为垃圾检测动作的起点,跟对象也就是一些全局引用和函数栈中的引用,这些引用所指向的对象是不可被删除的;2. 从root object集合出发,沿着root object集合中的每一个引用,如果能够到达某个对象,则说明这个对象是可达的,那么就不会被删除,这个过程就是垃圾检测阶段;3. 当检测阶段结束以后,所有的对象就分成可达和不可达两部分,所有的可达对象都进行保留,其它的不可达对象所占用的内存将会被回收,这就是垃圾回收阶段。(底层采用的是链表将这些集合的对象连接在一起)。
https://www.cnblogs.com/Xjng/p/5128269.html
https://www.jianshu.com/p/1e375fb40506
《垃圾回收的算法与实现》