python的优化机制与垃圾回收与gc模块

python属于动态语言,我们可以随意的创建和销毁变量,如果频繁的创建和销毁则会浪费cpu,那么python内部是如何优化的呢?

python和其他很多高级语言一样,都自带垃圾回收机制,不用我们去维护,也避免了出现内存泄漏,悬空指针等bug,那么python内部如何进行垃圾回收的呢?

python的垃圾回收,我们用gc模块去开启或者关闭它,那么gc模块又是什么呢?

 

python的优化机制

 

 

 

 

python垃圾回收之引用计数

 

关于循环引用进行一个测试

import gc  # python的一个垃圾回收模块,garbage collection


class A(object):
    def __init__(self):
        pass


def foo():
    while True:
        a1 = A()
        a2 = A()
        a1.t = a2
        a2.t = a1
        del a1
        del a2


gc.disable()
foo()

'''
当我们使用gc.disable(),就意味着我们把python的垃圾回收机制给关闭了,那么一些垃圾便不会被自动回收。
首先我们创建了两个实例对象a1和a2,然后在a1的内部创建一个属性指向a2,在a2的内部创建一个属性指向a1
那么a1和a2的引用计数都为2,即使del a1,del a2,它们的引用计数还是为1,因此仍然会待在内存里不会被回收
当我们进行第二轮循环的时候,会继续再内存中创建一份a1和a2,不断地循环会发现程序所使用的内存也来越大,直至溢出崩溃
'''

 

 

探讨一下ruby和python的垃圾回收机制
ruby和python均属于脚本语言,它们是非常相似的,比方说定义两个类
ruby的对象分配
当我们执行上面的Node.new(1)时,ruby在内部给我们做了什么?ruby是如何创建新的对象的呢?其实ruby做的很少,实际上早在代码执行之前,ruby就创建了成百上千个对象(类似于python的小整数对象池),并把它们串在链表上,叫做可用链表
因此在创建对象的时候直接拿来用就可以了。

白色的小方格相当于标着一个“未使用预创建对象”,当我们调用Node.new方法时,ruby直接取出一个给我们使用即可

可以简单地理解,黑色格子是当前使用的对象,白色格子是未使用对象。当我们创建了一个“ABC”,放在了第一个格子的位置,让n1这个变量指向它。

由于这是一个链表,链表上的对象一般保存着下一个对象的内存指针,因此我们只需要找到一个便可以顺藤摸瓜将剩下的全给找出来,由于已经被使用,那么“箭头”也被切断了,剩下的白色格子在一个链表上,保存的是可用的内存

如果我们再次调用Node.new,ruby将继续递给我们一个对象。

 

因此随着我们不断地创建,ruby不断的从可用链表里取预创建对象给我们,从而也就导致了可用链表会越来越短

注意图像,我为n1不止赋了一次值,赋完值之后,又给n1赋了新的值,让n1指向了新的内存空间,那么就会有垃圾产生。

ruby的标记--清除

当我们把可用空间全部用完了,可用链表上变得空空如也

可以看到所有格子都变灰了,没有白格子了。那么ruby是如何解决这一问题的呢?

ruby使用的是标记--清除,首先ruby会把程序停下来,然后轮询所有的指针,变量和代码产生的别的引用对象以及其他值,同时ruby通过自身的虚拟机遍历内部指针,只要对象被引用,那么便将其标记(mark)出来。

只要没有被标记的,那么便是垃圾。

 

ruby将这些没有被标记的垃圾进行回收,并把它们送回到可用链表当中。

由于内部发生的很快,ruby不会将对象到处拷贝,而是通过调整内部的指针,将其指向一个新链表的方式,来将垃圾对象归位到可用链表当中的。

 

 

 

python的对象分配
我们了解了ruby是预先创建对象并将它们放在可用链表当中,那么python又是怎样的呢?
在为新对象和变量分配内存方面,ruby和python是不同的

    我们用python来创一个Node对象

与ruby不同,python在创建对象时立即向操作系统申请内存(实际上python实现了一套自己的内存分配系统,在操作系统堆之上提供了一个抽象层,这方面的内容以后再说)

当我们创建第二个对象时,继续向操作系统申请内存。

 

给人感觉就像是,ruby在内存的申请和分配上先花了较长时间,接下来创建对象的时候效率高

python一开始并没有花费太长的时间,而是在创建的时候才会申请,相当于把时间均摊了。

 当我们有三个对象时

在创建对象时,python总是在对象的C结构体里保存一个整数,就是我们说的引用数,初始值为1

但是如果我们让n1指向新的对象

那么python变把“ABC”的引用数设置为0了,因此也就变成了垃圾。此时python内部不断运转的垃圾回收机制便闪亮登场了,当引用数变为0,python会立即将其释放,把内存空间交还给操作系统

 

因此ruby和python关于对象创建和垃圾回收的差异就是,ruby是一开始创建一大片,一大片都用完了再回收;python是用的时候才创建,用完了立刻回收。

可以理解为ruby是房间脏了不要紧,只要还能住人,然而当房间已经脏的没法住人了,会进行一次大扫除。python是只要脏了就会立刻进行打扫。

话说,ruby貌似是日本人发明的吧,举房间的例子貌似有点乖乖的,与我印象中的日本不太一样(*^__^*,话说如果大家以后有时间,推荐去一下日本(乡下),很美) 

 

python垃圾回收之隔代回收

 之前提到过循环引用

这种情况,python的引用计数是无法解决的,因此另一种垃圾清理方式便出现了,隔代回收

python在创建对象之前,也会创建一个链表,零代链表,只不过这个链表是空的。每当你创建一个对象,python便会将其加入到零代链表

当我们再创建一个对象时,python同样会将其加入到链表当中。

 

接下来检测循环引用

随后python会遍历零代链表上的每一个对象,检测链表中每个循环引用的对象,根据规则减去其引用计数。在这个过程中,python会一个接一个的统计内部对象的引用数量以防过早地释放对象。

从图中可以看到,有三个其他的对象同时存在于零代链表中,蓝色箭头表示一些对象正在被零代链表之外的某些对象引用着。python还存在一代和二代链表,这些对象有着更高的引用计数,因为它们正在被其他指针所指向着

如何处理零代链表:

从图中可以看到,"ABC","DEF"的引用计数为1,但是已经没有任何变量向它们了,因此python在达到一定条件(这里的条件后面再说)时,会将零代链表的具有循环引用的对象(python解释器不是上来就减去一,而是先判断哪些对象是被循环引用的)的引用计数统统减去1
如果引用计数为零,那么会将其作为垃圾清理掉,而那些没有被清理掉的,没有循环引用的(引用计数减一,可以看作忽视掉了一个循环引用)对象会被移动到一代链表中

同理,python还有二代链表。一代链表存放的是那些在零代链表中被清理完之后还活着的,二代链表存放的是那些在一代链表中被清理之后还活着的。为什么会有三条链子,而不是一条。

因为python在清理链子的时候,是把链子上的所有对象进行一次清理,如果已经清理一次感觉很稳定了,那么没有必要再清理了,因此将其移动到新的链子上。

因此python隔代回收的核心就是:对链子上的那些明明没有被引用但引用计数却不是零的对象进行引用计数减去一,看看你是不是垃圾。如果被引用多次减去一之后仍不为零,那么会在零代链表当中继续被清理,直至引用计数为零。因为如果没有变量指向它,或者作为函数的参数,列表的元素等等,那么它就始终是零代链表中被清理的对象。当零代链表被清理达到一定次数时(次数以及上面的条件在gc模块中会说),会清理一代链表。一代链表被清理达到一定次数时,会清理二代链表。

因此清理的频率最高的是零代链表,其次是一代链表,再是二代链表

因此关于python的垃圾回收用一句话总结就是:引用计数为主,隔代回收为辅,来完成垃圾回收。ruby则是标记清除

 

gc模块

再来看看这段代码,明明有垃圾回收,为什么程序的内存消耗还会不断地增加呢?因为在第19行,我们将gc(垃圾回收)的功能给关闭了

gc还可以显式的开启

因此这段程序也不会导致程序占用内存不断增加的情况产生

此外还可以通过gc.garbage,查看清理了哪些垃圾

不过gc一般默认是开启的,我们不用去关心它们。

 

话说还记的上面说的当达到一定条件清理零代链表,一代链表,二代链表吗?这个所谓的条件是什么呢?

在gc模块中有这样一个函数

 

这个元祖中有三个值,而且是不变的。第一个值700表示的是,当新创建的对象减去已经释放的对象超过700的时候,会清理零代链表

第二个值10表示的是,当零代链表清理10次的时候,会清理一次一代链表。在清理一代链表的时候,会顺便清理一次零代链表

第三个值10表示的是,当一代链表清理10次的时候,会清理一次二代链表。在清理二代链表的时候,会顺便清理一次零代链表和一代链表

也可以自己设置这个gc隔代回收触发条件的阈值

我们也可以查看当前的值是否达到了阈值

 

 

再来总结一下关于Python的引用计数

导致引用计数加一的情况:

  对象被创建,a=1

  对象被引用,b=a

  对象被作为参数传到一个函数中,func(a)

  对象作为列表,元祖等等的一个元素,li=[a,a]

导致引用计数减一的情况:

  对象的别名被显式的销毁,del a

  对象的引用指向了新的对象,a=2

  对象离开了它的作用域,比如函数的局部变量,在函数执行完毕的时候,也会被销毁。(全局变量不会)

  对象所在的容器被销毁,或着从容器中删除

查看一个对象的引用计数:

  通过sys模块下的一个函数

  

  不过这里为什么是2,难道不是1吗?这是因为我们把a扔进了sys.getrefcount()这个函数里,从而导致引用计数加1

  可以看一下源码

  

  源码的注释里也说了,返回一个对象的引用计数,但是这个数通常比你所预想的要多1,这是因为它包含了作为参数传给getrefcount()函数的引用。

  

 

 关于gc,还有一个重要的一点。

gc进行清理对象的时候,本质上调用了对象内部的析构函数__del__,但是如果我们重写了,那么它就不会执行自己的,或者父类的,那么对象也不会被清理掉。因此当我们在定义一个类,要重写析构函数时,当我们写完我们的代码时,最好让它再执行父类的析构方法。

这也算是gc的一个bug,不过这个bug只在python2中才会有,在我用的python3.6中不会出现这个bug,因此如果不是用python2的小伙伴的话,就不用担心这个问题啦·········

 

 

 

以上就是关于python的垃圾回收的一些理解··············

 

转载于:https://www.cnblogs.com/traditional/p/9205300.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值