垃圾回收机制
引入
解释器在执行到定义变量的语法时,会申请内存空间来存放变量的值,而内存的容量是有限的,这就涉及到变量值所占用内存空间的回收问题,当一个变量值没有用了(简称垃圾)就应该将其占用的内存给回收掉,那什么样的变量值是没有用的呢?
由于变量名是访问到变量值的唯一方式,所以当一个变量值不再关联任何变量名时,我们就无法再访问到该变量值了,该变量值就是没有用的,就应该被当成一个垃圾回收。毫无疑问,内存空间的申请与回收是非常耗费精力的事情,而且存在很大的危险性,稍有不慎就有可能引发内存溢出问题,好在Cpython解释器提供了自动的垃圾回收机制来帮我们解决了这件事
"""
有一些语言,内存空间的申请和释放都需要程序员自己写代码才可以完成
但是python却不需要 通过垃圾回收机制自动管理
"""
1.引用计数
引用计数就是:变量值被变量名关联的次数
eg:name = Li
变量值Li被关联了一个变量名name,那么就称之为引用计数为1
引用计数增加:
eg:name = Li # 此时数据值Li的引用计数为1
name1 = name # 把name的内存地址给了name1,此时name和name1都关联了数据值Li,所以数据值Li的引用计数为2
引用计数减少:
eg:name = Li # 此时数据值Li的引用计数为1
name1 = name # 此时数据值Li的引用计数为2
del name1 # del的意思是解除变量名name1与数据值Li的关联关系,此时数据值Li的引用计数为1
"""
当数据值身上的引用计数为0的时候,就会被垃圾回收机制当作垃圾回收掉
当数据值身上的引用计数不为0的时候,永远不会被垃圾回收机制回收
"""
2.标记清除
主要针对循环引用问题
l1 = [11, 22] # 引用计数为1
l2 = [33, 44] # 引用计数为1
l1.append(l2) # l1 = [11, 22, l2列表] 引用计数为2
l2.append(l1) # l2 = [33, 44, l1列表] 引用计数为2
del l1 # 断开变量名l1与列表的绑定关系 引用计数为1
del l2 # 断开变量名l2与列表的绑定关系 引用计数为1
循环引用会导致变量值不在被任何变量名关联,但是变量值的引用计数并不会为0,应该被回收但不能被回收。
因为此时列表1和列表二之间相互引用,两个列表的引用计数均不为0,但两个列表却不再被任何其他对象关联,没有任何人可以在引用到他们,所以他们两个占用内存空间应该被回收,但由于相互引用的存在,每个对象的引用计数都不为0,因此这些对象所占用的内存空间永远不会被释放,所以循环引用是致命的,这与手动进行内存管理所产生的内存泄露毫无区别。
所以Python引入了“标记-清除”与“分代回收”来分别解决引用计数的循环引用与效率低的问题
3.分代回收
背景:
基于引用计数的回收机制,每次回收内存,都需要把所有对象都查一遍,这是非常耗费时间的,于是引入了分代回收来提高效率。
分代:
分代回收的核心思想是:在历经多次扫描的情况下,都没有被回收的变量,垃圾回收机制就会认为,该变量是常用变量,垃圾回收机制对其扫描的频率会降低
我们可以根据存活时间来为变量分为不同等级(也就是不同的代)
新生代 青春代 老年代
新定义的变量,放到新生代这个等级中,假设每隔1分钟扫描新生代一次,如果发现变量依然被引用,那么该对象的权重(权重本质就是个整数)加一,当变量的权重大于某个设定得值(假设为3),会将它移动到更高一级的青春代,青春代的垃圾回收机制扫描的频率低于新生代(扫描时间间隔更长),假设5分钟扫描青春代一次,这样每次垃圾回收机制需要扫描的变量的总个数就变少了,节省了扫描的总时间,接下来,青春代中的对象,也会以同样的方式被移动到老年代中。也就是等级(代)越高,被垃圾回收机制扫描的频率越低