一、什么是Python垃圾回收机制
垃圾回收机制(简称GC)是Python解释器自带一种机制,专门用来回收不可用的变量值所占用的内存空间。每种语言都会有其特有的垃圾回收机制,以实现对计算机内存资源的最大利用。
二、为什么要有Python垃圾回收机制
因为Python程序在运行中会申请大量的内存空间(堆区),而当Python不再能够访问到这些内存空间时,这些内存空间就成了垃圾。若不对这些垃圾进行处理的话,随着程序的运行,内存就会被大量的垃圾所占据,最后导致运行缓慢,甚至宕机。不同于别的语言,Python解释器自带的垃圾回收机制,替程序员完成了堆区内存的申请与释放。
三、什么是Python垃圾回收机制
3.1 Python中变量存放规则
在Python中定义变量时,变量名和变量值都是需要存储的,分别对应内存中的两块区域:栈区与堆区。
PS:在C中概念
栈区:由系统自动分配释放,存放函数内的局部变量,形参和函数返回值。不需要开发人员手动管理。
堆区:由程序员调用malloc()来主动申请,使用free()来释放内存。若申请了堆区内存,之后忘记释放内存,很容易造成内存泄漏。若程序员不释放,在程序结束时,由操作系统回收。
经过这一段时间的学习,按照我的理解,Python中的变量都是在栈区申请的指针类型变量,并指向堆区中,Python解释器给该变量开辟的对应内存。其结构如下图(转自Egon老师的知乎):
x = 10
y = 20
当我们执行如下代码时候,将发生变化。x将在栈区重新申请一个空间来拷贝y变量存的内存地址(我认为是,在底层C语言中,x不知道收到的指针是什么类型,于是根据y的指针类型在栈区重新开辟空间):
x = y
3.2 直接引用和间接引用
直接引用是从栈区出发,直接引用到的内存地址。(栈—>堆)
间接引用是从栈区出发引用到堆区后,再通过进一步引用才能到达的内存地址。(堆—>堆)
如(转自Egon老师的知乎):
l2 = [20, 30] # 列表本身被变量名l2直接引用,包含的元素被列表间接引用
x = 10 # 值10被变量名x直接引用
l1 = [x, l2] # 列表本身被变量名l1直接引用,包含的元素被列表间接引用
四、垃圾回收机制的原理
主要机制包含以下三块:
1、引用计数:根据变量值被变量名关联的次数来跟踪和回收垃圾。(最主要)
2、标记-清除:解决出现循环引用,已无法访问的内存不能被释放问题。(优化)
3、分代回收:以空间换取时间的方法提高垃圾回收机制的效率。(优化)
4.1引用计数
age = 18 #此时值18的引用计数为1
m = age #此时值18的引用计数变为2
如下(转自Egon老师的知乎):
引用计数的减少:
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
循环引用会导致:值不再被任何栈区的变量名关联,但是值的引用计数不是0,此时无法根据第一种引用计数机制回收。操作如下:
>>> del l1 # 列表1的引用计数减1,列表1的引用计数变为1
>>> del l2 # 列表2的引用计数减1,列表2的引用计数变为1
此时两个列表的引用计数仍为1,但是两个列表不再被任何其他对象关联,我们是无法访问到这两个列表的。此时这两个列表无疑是垃圾,然而却无法被回收。一旦程序运行到一定程度,这种内存占用最终会导致程序的崩溃。
解决方案:
容器对象(比如:list,set,dict,class,instance)都可以包含对其他对象的引用,所以都可能产生循环引用。而“标记-清除”计数就是为了解决循环引用的问题。
标记/清除算法的做法是当应用程序可用的内存空间被耗尽的时,就会停止整个程序,然后进行两项工作,第一项则是标记,第二项则是清除
#1、标记
通俗地讲就是:
栈区相当于“根”,凡是从根出发可以访达(直接或间接引用)的,均可存活。
具体地:标记的过程其实就是,遍历所有的GC Roots对象(以栈区中的所有内容或者线程作为顶点,类似使用prim算法,遍历整个堆区顶点(一种图结构)),然后将所有GC Roots的对象可以直接或间接访问到的对象标记为存活的对象,其余的均为非存活对象,应该被清除。
#2、清除
清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。
5.3分代回收机制
问题:
由于基于引用计数的回收机制,每次回收内存,都需要把所有对象的引用计数都遍历一遍,这是非常消耗时间的,于是引入了分代回收来提高回收效率,分代回收采用的是用“空间换时间”的策略。
解决方案:
分代指的是根据存活时间来为变量划分不同等级(也就是不同的代)
新定义的变量,放到新生代这个等级中,假设每隔1分钟扫描新生代一次,如果发现变量依然被引用,那么该对象的权重(权重本质就是个整数)加一,当变量的权重大于某个设定得值(假设为3),会将它移动到更高一级的青春代,青春代的gc扫描的频率低于新生代(扫描时间间隔更长),假设5分钟扫描青春代一次,这样每次gc需要扫描的变量的总个数就变少了,节省了扫描的总时间,接下来,青春代中的对象,也会以同样的方式被移动到老年代中。也就是等级(代)越高,被垃圾回收机制扫描的频率越低
这种方法无疑可以提升效率,不过当一个新生代变量又或者一个青春代变量,在扫描后晋升,但立即便解除了绑定关系,这时就会有一定的延迟。不过综合来看,这种做法仍能大幅度提升效率,瑕不掩瑜。