|
|
51CTO旗下网站
|
|
移动端

聊一聊Python中的“垃圾”回收

对于python来说,一切皆为对象,所有的变量赋值都遵循着对象引用机制。程序在运行的时候,需要在内存中开辟出一块空间,用于存放运行时产生的临时变量;计算完成后,再将结果输出到永久性存储器中。

作者:heroyf来源:机器学习算法与Python学习|2019-12-02 16:23

前言

对于python来说,一切皆为对象,所有的变量赋值都遵循着对象引用机制。程序在运行的时候,需要在内存中开辟出一块空间,用于存放运行时产生的临时变量;计算完成后,再将结果输出到永久性存储器中。如果数据量过大,内存空间管理不善就很容易出现 OOM(out of memory),俗称爆内存,程序可能被操作系统中止。而对于服务器,内存管理则显得更为重要,不然很容易引发内存泄漏- 这里的泄漏,并不是说你的内存出现了信息安全问题,被恶意程序利用了,而是指程序本身没有设计好,导致程序未能释放已不再使用的内存。- 内存泄漏也不是指你的内存在物理上消失了,而是意味着代码在分配了某段内存后,因为设计错误,失去了对这段内存的控制,从而造成了内存的浪费。也就是这块内存脱离了gc的控制。

计数引用

因为python中一切皆为对象,你所看到的一切变量,本质上都是对象的一个指针。当一个对象不再调用的时候,也就是当这个对象的引用计数(指针数)为 0 的时候,说明这个对象永不可达,自然它也就成为了垃圾,需要被回收。可以简单的理解为没有任何变量再指向它。

  1. import os  
  2. import psutil   
  3.  
  4. # 显示当前 python 程序占用的内存大小 
  5.  
  6. def show_memory_info(hint):  
  7.     pid = os.getpid()  
  8.     p = psutil.Process(pid)  
  9.     info = p.memory_full_info()  
  10.     memory = info.uss / 1024.1024  
  11. print {} memory used: {} MB .format(hint, memory)) 

可以看到调用函数 func(),在列表 a 被创建之后,内存占用迅速增加到了 433 MB:而在函数调用结束后,内存则返回正常。这是因为,函数内部声明的列表 a 是局部变量,在函数返回后,局部变量的引用会注销掉;此时,列表 a 所指代对象的引用数为 0,Python 便会执行垃圾回收,因此之前占用的大量内存就又回来了。

  1. def func():  
  2.     show_memory_info( 
  3.  initial  
  4.  
  5. global 
  6. a = [i for  i in  range( 10000000 )]  
  7.     show_memory_info( after a created ) 
  8. func()  
  9. show_memory_info( 
  10.  finished  
  11. ) 
  12. ########## 输出 ##########  
  13. initial memory used: 48.88671875 MB  
  14. after a created memory used:433.94921875 MB  
  15. finished memory used:433.94921875 MB 

新的这段代码中,global a 表示将 a 声明为全局变量。那么,即使函数返回后,列表的引用依然存在,于是对象就不会被垃圾回收掉,依然占用大量内存。同样,如果我们把生成的列表返回,然后在主程序中接收,那么引用依然存在,垃圾回收就不会被触发,大量内存仍然被占用着:

  1. def func():  
  2.     show_memory_info(  initial  
  3.     a = [i for  i in  derange( 10000000 )]  
  4.     show_memory_info(  after a created 
  5.  
  6. return a  
  7. a = func() 
  8. show_memory_info( finished) 
  9.  
  10. ########## 输出 ##########  
  11. initial memory used:  47.96484375 MB 
  12. after a created memory used:434.515625 MB 
  13. finished memory used: 434.515625 MB 

那怎么可以看到变量被引用了多少次呢?通过 sys.getrefcount。

  1. import sys  
  2. a = []  
  3. # 两次引用,一次来自 a,一次来自 getrefcount 
  4. print (sys.getrefcount(a))  
  5.  
  6. def func(a):  
  7. # 四次引用,a,python 的函数调用栈,函数参数,和 getrefcount  
  8. print (sys.getrefcount(a))  
  9. func(a)  
  10. # 两次引用,一次来自 a,一次来自 getrefcount,函数 func 调用已经不存在  
  11. print (sys.getrefcount(a))   
  12. ########## 输出 ##########  
  13.  
  14.  

如果其中涉及函数调用,会额外增加两次1. 函数栈2. 函数调用。

从这里就可以看到python不再需要像C那种的认为的释放内存,但是python同样给我们提供了手动释放内存的方法 gc.collect()。

  1. import gc  
  2. show_memory_info( initial 
  3. a = [i for  i in range( 10000000 )]  
  4. show_memory_info(  after a created
  5. del a 
  6. gc.collect() 
  7. show_memory_info( finish  
  8. print (a)  
  9. ########## 输出 ########## 
  10. initial memory used: 48.1015625 MB 
  11. after a created memory used: 434.3828125 MB  
  12. finish memory used: 48.33203125 MB 
  13. --------------------------------------------------------------------------- 
  14. NameErrorTraceback (most recent call last
  15.  
  16. <ipython-input- 12 153e15063d8a > in<module>  
  17. 11  
  18. 12 show_memory_info(  finish ) 
  19. --->  13 print (a) 
  20.  
  21. NameError : name  a  isnotdefined 

截止目前,貌似python的垃圾回收机制非常的简单,只要对象引用次数为0,必定为触发gc,那么引用次数为0是否是触发gc的充要条件呢?

循环回收

如果有两个对象,它们互相引用,并且不再被别的对象所引用,那么它们应该被垃圾回收吗?

  1. def func(): 
  2.     show_memory_info( initial  
  3.     a = [i for  i in  range(10000000)] 
  4.     b = [i for  in  range(10000000)]  
  5.     show_memory_info(  after a, b created  
  6.     a.append(b)  
  7.     b.append(a) 
  8. func() 
  9. show_memory_info(  finished  
  10. ########## 输出 ##########  
  11. initial memory used: 47.984375 MB  
  12. after a, b created memory used:822.73828125 MB  
  13. finished memory used:  821.73046875 MB 

从结果显而易见,它们并没有被回收,但是从程序上来看,当这个函数结束的时候,作为局部变量的a,b就已经从程序意义上不存在了。但是因为它们的互相引用,导致了它们的引用数都不为0。这时要如何规避呢1. 从代码逻辑上进行整改,避免这种循环引用2. 通过人工回收。

  1. import gc 
  2. def func():  
  3.     show_memory_info( initial 
  4.     a = [i for  i in  range(10000000)]  
  5.     b = [i for  i in  range(10000000)] 
  6.     show_memory_info( after a, b created 
  7.     a.append(b) 
  8.     b.append(a) 
  9. func() 
  10. gc.collect() 
  11. show_memory_info( finished  
  12. ########## 输出 ##########  
  13. initial memory used:49.51171875 MB  
  14. after a, b created memory used: 824.1328125 MB  
  15. finished memory used:49.98046875 MB 

python针对循环引用,有它的自动垃圾回收算法1. 标记清除(mark-sweep)算法2. 分代收集(generational)。

标记清除

标记清除的步骤总结为如下步骤1. GC会把所有的『活动对象』打上标记2. 把那些没有标记的对象『非活动对象』进行回收那么python如何判断何为非活动对象?通过用图论来理解不可达的概念。对于一个有向图,如果从一个节点出发进行遍历,并标记其经过的所有节点;那么,在遍历结束后,所有没有被标记的节点,我们就称之为不可达节点。显而易见,这些节点的存在是没有任何意义的,自然的,我们就需要对它们进行垃圾回收。但是每次都遍历全图,对于 Python 而言是一种巨大的性能浪费。所以,在 Python 的垃圾回收实现中,mark-sweep 使用双向链表维护了一个数据结构,并且只考虑容器类的对象(只有容器类对象,list、dict、tuple,instance,才有可能产生循环引用)。

图中把小黑圈视为全局变量,也就是把它作为root object,从小黑圈出发,对象1可直达,那么它将被标记,对象2、3可间接到达也会被标记,而4和5不可达,那么1、2、3就是活动对象,4和5是非活动对象会被GC回收。

分代回收

分代回收是一种以空间换时间的操作方式,Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时(当垃圾回收器中新增对象减去删除对象达到相应的阈值时),Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。同时,分代回收是建立在标记清除技术基础之上。事实上,分代回收基于的思想是,新生的对象更有可能被垃圾回收,而存活更久的对象也有更高的概率继续存活。因此,通过这种做法,可以节约不少计算量,从而提高 Python 的性能。所以对于刚刚的问题,引用计数只是触发gc的一个充分非必要条件,循环引用同样也会触发。

调试

可以使用 objgraph来调试程序,因为目前它的官方文档,还没有细读,只能把文档放在这供大家参阅啦~其中两个函数非常有用 1. show_refs() 2. show_backrefs()。

【责任编辑:庞桂玉 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

骨干网与数据中心建设案例

骨干网与数据中心建设案例

高级网工必会
共20章 | 捷哥CCIE

403人订阅学习

中间件安全防护攻略

中间件安全防护攻略

4类安全防护
共4章 | hack_man

151人订阅学习

CentOS 8 全新学习术

CentOS 8 全新学习术

CentOS 8 正式发布
共16章 | UbuntuServer

291人订阅学习

读 书 +更多

软件架构设计

本书紧紧围绕“软件架构设计”这一主题,立足实践解析了软件架构的概念,阐述了切实可行的软件架构设计方法,提供了可操作性极强的完整的架...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微