Garbage collection(GC)
现在的高级语言如java,c#等,都采用了垃圾收集机制,而不再是c,c++里用户自己管理维护内存的方式。自己管理内存极其自由,可以任意申请内存,但如同一把双刃剑,为大量内存泄露,悬空指针等bug埋下隐患。
对于一个字符串、列表、类甚至数值都是对象,且定位简单易用的语言,自然不会让用户去处理如何分配回收内存的问题。
python里也同java一样采用了垃圾收集机制,不过不一样的是:
python采用的是引用计数机制为主,标记-清除和分代收集两种机制为辅的策略
1、引用计数机制:
python里每一个东西都是对象,它们的核心就是一个结构体:PyObject
typedef struct_object {
int ob_refcnt;
struct_typeobject *ob_type;
} PyObject;
PyObject是每个对象必有的内容,其中ob_refcnt就是做为引用计数。当一个对象有新的引用时,它的ob_refcnt就会增加,当引用它的对象被删除,它的ob_refcnt就会减少
#define Py_INCREF(op) ((op)->ob_refcnt++) //增加计数
#define Py_DECREF(op) \ //减少计数
if (--(op)->ob_refcnt != 0) \
; \
else \
__Py_Dealloc((PyObject *)(op))
当引用计数为0时,该对象生命就结束了。
引用计数机制的优点:
简单
实时性:一旦没有引用,内存就直接释放了。不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时。
引用计数机制的缺点:
维护引用计数消耗资源
循环引用
list1 = []
list2 = []
list1.append(list2)
list2.append(list1)
list1与list2相互引用,如果不存在其他对象对它们的引用,list1与list2的引用计数也仍然为1,所占用的内存永远无法被回收,这将是致命的。
对于如今的强大硬件,缺点1尚可接受,但是循环引用导致内存泄露,注定python还将引入新的回收机制。(标记清除和分代收集)
2、标记-清理
由上面内容我们可以知道,引用计数机制有两个缺点,缺点1还可以勉强让人接受,缺点2如果不解决,肯定会引起内存泄露,为了解决这个问题,引入了标记删除。
我们先来看个实例,从实例中领会标记删除:
a=[1,2]#假设此时a的引用为1
b=[3,4]#假设此时b的引用为1 #循环引用
a.append(b)#b的引用+1=2
b.append(a)//a的引用+1=2
假如现在需要删除a,应该如何回收呢?(注意删除a可以使用del a,这样a这个引用就不存在了,但是它指向的对象,在标记删除后还存在,因为还被b使用者)
c=[5,6]#假设此时c的引用为1
d=[7,8]#假设此时d的引用为1 #循环引用
c.append(d)#c的引用+1=2
d.append(c)#d的引用+1=2 假如现在需要同时删除c、d,应该如何回收呢?
首先我们应该已经知道,不管上面两种情况的哪一个都无法只通过计数来完成回收,因为随便删除一个变量,它的引用只会-1,变成1,还是大于0,不会回收,为了解决这个问题,开始看标记删除来大展神威吧。
python标记删除时通过l两个容器来完成的:死亡容器、存活容器。
首先,我们先来分析情况2,删除c、d
删除后,c的引用为1,d的引用为1,根据引用计数,还无法删除
标记删除第一步:对执行删除操作后的每个引用-1,此时c的引用为0,d的引用为0,把他们都放到死亡容器内。把那些引用仍然大于0的放到存活容器内。
标记删除第二步:遍历存活容器,查看是否有的存活容器引用了死亡容器内的对象,如果有就把该对象(注意是对象,比如0x7f94bb602f80,不是对象的引用)从死亡容器内取出,放到存活容器内。
由于c、d都没有对象引用他们了,所以经过这一步骤,他们还是在死亡组。
标记删除第三部:将死亡组所有对象删除。
这样就完成了对从c、d的删除。
同样道理,我们来分析:只删除a的过程:
标记删除第一步:对执行删除(-1)后的每个引用-1,那么a的引用就是0,b的引用为1,将a放到死亡容器,将b放到存活容器。
标记删除第二步:循环存活容器,发现b引用a,复活a:将a放到存活容器内。
标记删除第三步:删除死亡容器内的所有对象。
综上所说,发现对于循环引用,必须将循环引用的双发对象都删除,才可以被回收。
标记-清理就是这样。
3、分代收集
经过上面的【标记-清理】方法,已经可以保证对垃圾的回收了,但还有一个问题,【标记-清理】什么时候执行比较好呢,是对所有对象都同时执行吗?
同时执行很显然不合理,我们知道,存活越久的对象,说明他的引用更持久(好像是个屁话,引用不持久就被删除了),为了更合理的进行【标记-删除】,就需要对对象进行分代处理,思路很简单:
1、新创建的对象做为0代
2、每执行一个【标记-删除】,存活的对象代数就+1
3、代数越高的对象(存活越持久的对象),进行【标记-删除】的时间间隔就越长。这个间隔,江湖人称阀值。
是不是很简单呢。
4、三种情况触发垃圾回收
1、调用gc.collect()
2、GC达到阀值时
3、程序退出时
5、小整数对象池与intern机制:这个机理是有的,但是下文的数据有待认证
由于整数使用广泛,为了避免为整数频繁销毁、申请内存空间,引入了小整数对象池。[-5,257)是提前定义好的,不会销毁,单个字母也是。
那对于其他整数,或者其他字符串的不可变类型,如果存在重复的多个,例如:
a1="mark"
a2="mark"
a3="mark"
a4="mark"
....
a1000="mark"
如果每次声明都开辟出一段空间,很显然不合理,这个时候python就会使用intern机制,靠引用计数来维护。
总计:
1、小整数[-5,257):共用对象,常驻内存
2、单个字符:共用对象,常驻内存
3、单个单词等不可变类型,默认开启intern机制,共用对象,引用计数为0时销毁。
————————————————
版权声明:本文为CSDN博主「LuckyQueen0928」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/luckyqueen0928/article/details/96044552