Python中垃圾回收机制的理解
定义:
垃圾回收:Garbage Collection
现在的高级语言如java,c#等,都采用了垃圾收集机制,而不再是c,c++里用户自己管理维护内存的方式。自己管理内存极其自由,可以任意申请内存,但如同一把双刃剑,为大量内存泄露,悬空指针等bug埋下隐患。 对于一个字符串、列表、类甚至数值都是对象,且定位简单易用的语言,自然不会让用户去处理如何分配回收内存的问题。 python里也同java一样采用了垃圾收集机制,不过不一样的是: python采用的是引用计数机制为主,分代收集机制为辅的策略。
Num01–>小整数对象池
整数在程序中的使用非常广泛,Python为了优化速度,使用了小整数对象池, 避免为整数频繁申请和销毁内存空间。
Python 对小整数的定义是 [-5, 256] 这些整数对象是提前建立好的,不会被垃圾回收。在一个 Python 的程序中,所有位于这个范围内的整数使用的都是同一个对象.
同理,单个字母也是这样的。
但是当定义2个相同的字符串时,引用计数为0时,才触发垃圾回收。
Num02–>大整数对象池(即小整数对象池之外的数)
每一个大整数,均创建一个新的对象。
Num03–>intern机制
a1 = "xiaoke"
a2 = "xiaoke"
a3 = "xiaoke"
a4 = "xiaoke"
a5 = "xiaoke"
a6 = "xiaoke"
python会不会创建6个对象呢?在内存中会不会开辟6个”xiaoke”的内存空间呢? 想一下,如果是这样的话,我们写10000个对象,比如a1=”xiaoke” ….. a1000=”xiaoke”, 那他岂不是开辟了1000个”xiaoke”所占的内存空间了呢?如果真这样,内存不就爆了吗?所以python中有这样一个机制——intern机制,让他只占用一个”xiaoke”,所占的内存空间。靠引用计数去维护何时释放。
小总结:
1,小整数[-5,256]共用对象,常驻内存
2,单个字符共用对象,常驻内存
3,单个单词,不可修改,默认开启intern机制,共用对象,引用计数为0,则销毁
4,字符串(含有空格),不可修改,没开启intern机制,不共用对象,引用计数为0,销毁
5, 大整数不共用内存,引用计数为0,销毁
6,数值类型和字符串类型在 Python 中都是不可变的,这意味着你无法修改这个对象的值,每次对变量的修改,实际上是创建一个新的对象
Num04–>引用计数机制
Python中每一个东西都是对象,它们的核心就是一个结构体:PyObject
typedef struct_object {
Py_ssize_t ob_refcnt;
struct_typeobject *ob_type;
} PyObject;
PyObject是每个对象必有的内容。可以把Py_ssize_t 当做int类型就好。其中ob_refcnt就是用做引用计数。当一个对象有新的引用时,ob_refcnt就会增加;当引用它的对象被删除时,它的ob_refcnt就会减少。
当引用计数为0时,该对象生命就结束了。
引用计数的优点:
1,简单
2,实时性:一旦没有引用,内存就直接释放了。实时性还带来一个好处:处理回收内存的时间分摊到了平时。
引用计数的缺点:
1,维护引用计数消耗资源。
2,循环引用带来的问题,没有解决。如下案例:
list1 = []
list2 = []
list1.append(list2)
list2.append(list1)
list1与list2相互引用,如果不存在其他对象对它们的引用,list1与list2的引用计数也仍然为1,所占用的内存永远无法被回收,这将是致命的。 对于如今的强大硬件,缺点1尚可接受,但是循环引用导致内存泄露,注定python还将引入新的回收机制:分代收集。
Num05–>分代收集垃圾机制
第一, Python使用一种不同的链表来持续追踪活跃的对象,Python的内部C代码将其称为零代链表(Generation Zero)。每次当你创建一个什么对象或其他什么值的时候,Python会将其添加到零代链表。
第二,随后,Python会循环遍历零代列表上的每个对象,找出列表中每个互相引用的对象,根据规则减掉其引用计数。在这个过程中,Python会一个接一个的统计内部引用的数量以防过早地释放它们。
第三,通过识别内部引用,Python能够减少许多零代链表循环引用的对象,这意味着回收器可以释放它们并回收内存了。剩下的活跃对象则被移动到一个新的链表:一代链表。
第四,周期性地从一个对象到另一个对象追踪引用以确定对象是否还是活跃的,正在被程序所使用的。清理完一代链表后,把活跃的对象,或者正在被程序使用的对象,移动到二代链表中,最终在二代链表中清除数据。这就是所谓的分代收集垃圾机制。
Num06–>Python中的GC阀值
1,Python什么时候会进行这个标记过程?随着你的程序运行,Python解释器保持对新创建的对象,以及因为引用计数为零而被释放掉的对象的追踪。从理论上说,这两个值应该保持一致,因为程序新建的每个对象都应该最终被释放掉。
2,当然,事实并非如此。因为循环引用的原因,并且因为你的程序使用了一些比其他对象存在时间更长的对象,从而被分配对象的计数值与被释放对象的计数值之间的差异在逐渐增长。一旦这个差异累计超过某个阈值,则Python的收集机制就启动了,并且触发上边所说到的零代算法,释放“浮动的垃圾”,并且将剩下的对象移动到一代列表。
3,随着时间的推移,程序所使用的对象逐渐从零代列表移动到一代列表。而Python对于一代列表中对象的处理遵循同样的方法,一旦被分配计数值与被释放计数值累计到达一定阈值,Python会将剩下的活跃对象移动到二代列表。
4,通过这种方法,你的代码所长期使用的对象,那些你的代码持续访问的活跃对象,会从零代链表转移到一代再转移到二代。通过不同的阈值设置,Python可以在不同的时间间隔处理这些对象。Python处理零代最为频繁,其次是一代然后才是二代。
Num07–>查看一个对象的引用计数
import sys
name = "xiaoke"
print(sys.getrefcount(name))
可以查看name对象的引用计数,但是比正常计数大1,因为调用函数的时候传入name,这会让name的引用计数+1
Num08–>GC模块的自动垃圾回收触发机制
在Python中,采用分代收集的方法。把对象分为三代,一开始,对象在创建的时候,放在一代中,如果在一次一代的垃圾检查中,改对象存活下来,就会被放到二代中,同理在一次二代的垃圾检查中,该对象存活下来,就会被放到三代中。
gc模块里面会有一个长度为3的列表的计数器,可以通过gc.get_count()获取当前阀值。
例如(666,6,0),其中666是指距离上一次一代垃圾检查,Python分配内存的数目减去释放内存的数目,注意是内存分配,而不是引用计数的增加;
66是指距离上一次二代垃圾检查,一代垃圾检查的次数;
同理,0是指距离上一次三代垃圾检查,二代垃圾检查的次数。
看如下案例:
class XiaoKe(object):
pass
print(gc.get_count()) # (560, 10, 0)
xiaoke=XiaoKe()
print(gc.get_count()) # (561, 10, 0)
del xiaoke
print(gc.get_count()) # (560, 10, 0)
gc模快有一个自动垃圾回收的阀值,即通过gc.get_threshold函数获取到的长度为3的元组(700,10,10)。每一次计数器的增加,gc模块就会检查增加后的计数是否达到阀值的数目,如果是,就会执行对应的代数的垃圾检查,然后重置计数器
例如,假设阀值是(700,10,10):
1,当计数器从(699,6,0)增加到(700,6,0),gc模块就会执行gc.collect(0),即检查一代对象的垃圾,并重置计数器为(0,7,0)
2,当计数器从(699,9,0)增加到(700,9,0),gc模块就会执行gc.collect(1),即检查一、二代对象的垃圾,并重置计数器为(0,0,1)
3,当计数器从(699,9,9)增加到(700,9,9),gc模块就会执行gc.collect(2),即检查一、二、三代对象的垃圾,并重置计数器为(0,0,0)
Num09–>什么时候触发垃圾回收机制
1,当gc模块的计数器达到阀值的时候,自动回收垃圾。
2,手动调用gc.collect(),手动回收垃圾
3,程序退出的时候,Python解释器会回收垃圾。