现在的高级语言如java,c#等,都采用了垃圾收集机制,而不再是c,c++里用户自己管理维护内存的方式。但是这种方式如同一把双刃剑,一方面可以自由的支配内存,另一方面也造成了大量的内存泄露。python里也同java一样采用了垃圾收集机制,不过不一样的是: python采用的是引用计数机制为主,标记-清除和分代收集两种机制为辅的策略。
引用计数机制:
在python的世界里一切皆对象,在创建每一个对象时,就会同时为其建立一个引用计数来追踪到底有多少引用指向这个对象,对象一创建,它的引用计数就为1,当这个对象有新的引用时,其引用计数就会加1,引用每减少一个,其引用计数就会减1,当引用计数减少为0,即没有变量再引用这个对象时,这个对象就会被python自动的垃圾回收,释放其内存空间。
但是python中并非所有的对象在没有变量引用时就会被垃圾回收,python中有一个内置的小整数常量池[-5~257),其中的小整数都是常驻内存的,即使没有对象再引用这些小整数,它们也不会被垃圾回收。·单个的字符也是这样,共享引用,常驻内存。但对于不可修改的单个单词来说,默认采用的是intern机制,intern机制其实就是共享引用和引用计数机制的一个结合,假如a = 'hello',再定义一个b = 'hello',再定义一个c = 'hello',其实a,b,c这三个变量都是指向同一个‘hello’对象,它们的id 都是相同的,此时,‘hello’的引用计数就为3,当切断任一个引用,计数就会减1,当计数减为0时,这个'hello'就会被垃圾回收。
导致引用计数加1的情况:
·1、对象被创建,例如a=23
·2、对象被引用,例如b=a
·3、对象被作为参数,传入到一个函数中,例如func(a)
·4、对象作为一个元素,存储在容器中,例如list1=[a,a]
导致引用计数减1的情况:
·1、对象的别名被显式销毁,例如del a
·2、对象的别名被赋予新的对象,例如a=24
·3、一个对象离开它的作用域,例如f函数执行完毕时,func函数中的局部变量(全局变量不会)
·4、对象所在的容器被销毁,或从容器中删除对象
引用计数机制的优点:1、简单; 2、实时性:一旦没有引用,内存就直接释放了。不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时。
引用计数机制的缺点:1、每一个对象都要建立引用计数,维护引用计数消耗资源;2、付出了一小点儿空间上的代价。但更糟糕的是,每个简单的操作(像修改变量或引用)都会变成一个更复杂的操作,因为Python需要增加一个计数,减少另一个,还可能释放对象;3、存在循环引用时就会长时间的占用资源而不能被回收,解决不了循环引用是垃圾回收的问题;4、运行速度比较慢。
标记-清除机制:
在标记清除机制下,在创建对象时,只管开辟内存空间,而不考虑资源的释放,那么这样以来,总有一个时刻,内存空间会被全部占用,到再创建对象时,发现已经没有资源可以被利用了,这个时候,就会轮询所有指针、变量和代码产生别的引用对象和其他值,标记出这些指针引用的每个对象。
图1上图中,灰色的方框代表已经被占用的内存资源,M表示标记出这些指针引用的每个对象,被标M的对象表示有程序还在使用。
如果说被标记M的对象是还在存活着的,那么其余的对象就是垃圾。那么接下来就要对这些垃圾进行回收,把它们重新送回可以列表中,这一切在内部都发生的迅雷不及掩耳。
上面我们已经介绍了垃圾回收机制,那么现在再来看这样一种情况,如果一个数据结构引用了它自身,即如果这个数据结构是一个循环数据结构,那么某些引用计数值是肯定无法变成零的,为了更好地理解这个问题,让我们举个例子:
显然,上图中的"ABC"和"DEF"两个对象的引用计数永远不可能为0,除非人为将这两个引用切除,这样以来就体现出了引用计数机制的弊端,它无法解决循环引用的问题。为了尽可能改善这一弊端,Python引入了Generational GC算法。
在GC算法中,使用一种不同的链表来持续追踪活跃的对象,Python的内部C代码将其称为零代(Generation Zero)。每次当你创建一个对象或其他什么值的时候,Python会将其加入零代链表:
注意这并不是一个真正的列表,并不能直接在你的代码中访问,事实上这个链表是一个完全内部的Python运行时。现在零代包含了两个节点对象。(他还将包含Python创建的每个其他值,与一些Python自己使用的内部值)
随后,Python会循环遍历零代列表上的每个对象,检查列表中每个互相引用的对象,根据规则减掉其引用计数。在这个过程中,Python会一个接一个的统计内部引用的数量以防过早地释放对象。
为了方便理解下面看一个例子:
从上面可以看到ABC和DEF节点包含的引用数为1,有三个其他的对象同时存在于零代链表中,蓝色的箭头指示了有一些对象正在被零代链表之外的其他对象所引用。(接下来我们会看到,Python中同时存在另外两个分别被称为一代和二代的链表)。这些对象有着更高的引用计数因为它们正在被其他指针所指向着。通过识别内部引用,Python能够减少许多零代链表对象的引用计数。在上图的第一行中你能够看见ABC和DEF的引用计数已经变为零了,这意味着收集器可以释放它们并回收内存空间了。剩下的活跃的对象则被移动到一个新的链表:一代链表。
从某种意义上说,Python的GC算法类似于标记-清除算法,周期性地从一个对象到另一个对象追踪引用以确定对象是否还是活跃的,正在被程序所使用的。
那么,Python什么时候会进行这个标记过程呢?可能因为循环引用的原因,并且因为你的程序使用了一些比其他对象存在时间更长的对象,从而被分配对象的计数值与被释放对象的计数值之间的差异在逐渐增长。一旦这个差异累计超过某个阈值,则Python的收集机制就启动了,并且触发上边所说到的零代算法,释放“浮动的垃圾”,并且将剩下的对象移动到一代列表。
随着时间的推移,程序所使用的对象逐渐从零代列表移动到一代列表。而Python对于一代列表中对象的处理遵循同样的方法,一旦被分配计数值与被释放计数值累计到达一定阈值,Python会将剩下的活跃对象移动到二代列表。
通过这种方法,你的代码所长期使用的对象,那些你的代码持续访问的活跃对象,会从零代链表转移到一代再转移到二代。通过不同的阈值设置,Python可以在不同的时间间隔处理这些对象。Python处理零代最为频繁,其次是一代然后才是二代。
零代垃圾回收算法的核心行为:垃圾回收器会更频繁的处理新对象。一个新的对象即是你的程序刚刚创建的,而一个旧的对象则是经过了几个时间周期之后仍然存在的对象。Python会在当一个对象从零代移动到一代,或是从一代移动到二代的过程中提升(promote)这个对象,这就是所谓的年轻的对象通常死得也快,而老对象则很有可能存活更长的时间。
Python的垃圾收集器将把时间花在更有意义的地方:它处理那些很快就可能变成垃圾的新对象。同时只在很少的时候,当满足阈值的条件,收集器才回去处理那些老变量。
查看一个对象的引用计数:
,除了a,b引用‘hello world’之外,调用sys.getrefcount(a)也会使其引用计数加1,切断b的引用,会使其计数减1。
gc模块常用功能:
1、gc.set_debug(flags):设置gc的debug日志,一般设置为gc.DEBUG_LEAK
2、gc.collect([generation]):显式进行垃圾回收,可以输入参数,0代表只检查零代的对象,1代表检查零代、一代的对象,2代表检查零、一、二代的对象,如果不传参数,执行一个full collection,也就是等于传2。 返回不可达(unreachable objects)对象的数目
3、gc.get_threshold():获取gc模块中自动执行垃圾回收的频率,此函数获取到的长度为3的元组,每一次计数器的增加,gc模块就会检查增加后的计数是否达到阀值的数目,如果是,就会执行对应的代数的垃圾检查,然后重置计数器
当计数器从(699,3,0)增加到(700,3,0),gc模块就会执行gc.collect(0),即检查零代对象的垃圾,并重置计数器为(0,4,0)
当计数器从(699,9,0)增加到(700,9,0),gc模块就会执行gc.collect(1),即检查零、一代对象的垃圾,并重置计数器为(0,0,1)
当计数器从(699,9,9)增加到(700,9,9),gc模块就会执行gc.collect(2),即检查零、一、二代对象的垃圾,并重置计数器为(0,0,0)
4、gc.set_threshold(threshold0[, threshold1[, threshold2])设置自动执行垃圾回收的频率。
5、gc.get_count() 获取当前自动执行垃圾回收的计数器,返回一个长度为3的列表
其中21是指距离上一次零代垃圾检查,Python分配内存的数目减去释放内存的数目,注意是内存分配,而不是引用计数的增加,如
第一个0指的是指距离上一次零代垃圾检查,一代垃圾检查的次数,同理,第二个0是指距离上一次一代垃圾检查,二代垃圾检查的次数。
注意:gc模块唯一处理不了的是循环引用的类都有__del__方法,所以项目中要避免定义__del__方法。
触发垃圾回收的三种情况:
1、调用gc.collect();
2、当gc模块的计数器达到阀值的时候;
3、程序退出的时候
网友评论