每天反省我的身体,我是什么样的垃圾?
生活中饱受垃圾分类之苦的各位,今天就来谈谈Python的垃圾回收机制
我们知道Python程序在运行时需要在内存中留出空间来存储运行时发生的临时变量。计算完成后,它会将结果输出到非易失性存储器。 如果数据量太大,内存空间管理不良就容易产生内存输出(oom ),俗称爆炸内存,操作系统可能会中止程序。
对于服务器,内存管理更为重要,因为它旨在避免中断此类系统。 否则,很容易发生内存泄漏。 什么是内存泄漏?
这里的泄露并不是指你的内存出现了信息安全问题,被恶意程序利用了,而是指程序本身没有设计,无法释放不再使用的内存。
内存泄漏并不是指你的内存物理消失,而是指分配了包含代码的内存后,由于设计错误而失去了对内存的控制,导致内存浪费。
那么Python是怎么解决这些问题的呢? 换句话说,对于不再使用的内存空间,Python是通过什么机制回收这些空间的呢?
引用数
我重复了好几次,在Python上一切都是对象。 因此,你看到的所有变量本质上都是对象的指针。
那么,你怎么知道一个对象,会不会永远被呼叫?
当对象的参照计数(指针的数目)为0时,该对象决不能到达,这当然也成为垃圾,表示该对象需要被回收。
请看一个例子:
导入操作系统
导入PS util
#显示当前python程序消耗的内存大小
efshow_memory_info(hint ) :
pid=os.getpid (
p=psutil.Process(pid )
info=p.memory_full_info (
memory=info.uss/1024./1024
print((memoryused: ) MB ).format (hint,memory ) )。
代码def func (复制:
show_memory_info(initial ) )。
a=[IforIinrange(1000000 ) ]
show _ memory _ info (已关联) )。
func () )
show _ memory _ info (完成) )。
# # # #
initialmemoryused 336047.19140625 MB
afteracreatedmemoryused 3360433.91015625 MB
finishedmemoryused :48.109375 MB
复制代码
在此示例中,通过调用函数func (),可以看到在创建列表a后,内存使用量迅速增加到433 MB。 函数调用结束后,内存恢复正常。
这是因为在函数内部声明的列表a是局部变量,在函数返回后,局部变量的参照被删除; 此时,如果列表a指向的对象引用数为0,则Python将执行垃圾回收,从而返回以前消耗的大量内存。
理解了这个原理后,稍微修改一下代码:
deffunc(:
show_memory_info(initial ) )。
全球a
a=[IforIinrange(1000000 ) ]
show _ memory _ info (已关联) )。
func () )
show _ memory _ info (完成) )。
# # # #
initialmemoryused 336048.88671875 MB
afteracreatedmemoryused 3360433.94921875 MB
finishedmemoryused :433.94921875 MB
复制代码
在新代码中,global a表示将a声明为全局变量。 在中,即使函数返回,对列表的引用仍然保留,因此不会进行垃圾回收,仍然占用大量内存。
同样,如果在主程序中返回生成的列表并将其接收,则引用仍然存在,不会触发垃圾回收,并且会消耗大量内存。
deffunc(:
show_memory_info(initial ) )。
a=[IforIinderange(1000000 ) ]
show _ memory _ info (已关联) )。
返回a
a=func ()
show_memory_info
('finished')########## 输出 ##########
initial memory used: 47.96484375 MB
after a created memory used: 434.515625 MB
finished memory used: 434.515625 MB
复制代码
这是最常见的几种情况。由表及里,下面,我们深入看一下 Python 内部的引用计数机制。老规矩,先来看代码:
import sys
a = []
# 两次引用,一次来自 a,一次来自 getrefcount
print(sys.getrefcount(a))
def func(a):
# 四次引用,a,python 的函数调用栈,函数参数,和 getrefcount
print(sys.getrefcount(a))
func(a)
# 两次引用,一次来自 a,一次来自 getrefcount,函数 func 调用已经不存在
print(sys.getrefcount(a))
########## 输出 ##########
2
4
2
复制代码
简单介绍一下,sys.getrefcount() 这个函数,可以查看一个变量的引用次数。这段代码本身应该很好理解,不过别忘了,getrefcount 本身也会引入一次计数。
另一个要注意的是,在函数调用发生的时候,会产生额外的两次引用,一次来自函数栈,另一个是函数参数。
import sys
a = []
print(sys.getrefcount(a)) # 两次
b = a
print(sys.getrefcount(a)) # 三次
c = b
d = b
e = c
f = e
g = d
print(sys.getrefcount(a)) # 八次
########## 输出 ##########
2
3
8
复制代码
看到这段代码,需要你稍微注意一下,a、b、c、d、e、f、g 这些变量全部指代的是同一个对象,而 sys.getrefcount() 函数并不是统计一个指针,而是要统计一个对象被引用的次数,所以最后一共会有八次引用。
理解引用这个概念后,引用释放是一种非常自然和清晰的思想。相比 C 语言里,你需要使用 free 去手动释放内存,Python 的垃圾回收在这里可以说是省心省力了。
不过,我想还是会有人问,如果我偏偏想手动释放内存,应该怎么做呢?
方法同样很简单。你只需要先调用 del a 来删除对象的引用;然后强制调用 gc.collect(),清除没有引用的对象,即可手动启动垃圾回收。
import gc
show_memory_info('initial')
a = [i for i in range(10000000)]
show_memory_info('after a created')
del a
gc.collect()
show_memory_info('finish')
print(a)
########## 输出 ##########
initial memory used: 48.1015625 MB
after a created memory used: 434.3828125 MB
finish memory used: 48.33203125 MB
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
in
11
12 show_memory_info('finish')
---> 13 print(a)
NameError: name 'a' is not defined
复制代码
到这里,是不是觉得垃圾回收非常简单呀?
我想,肯定有人觉得自己都懂了,那么,如果此时有面试官问:引用次数为 0 是垃圾回收启动的充要条件吗?还有没有其他可能性呢?
这个问题,你能回答的上来吗?
循环引用
如果你也被困住了,别急。我们不妨小步设问,先来思考这么一个问题:如果有两个对象,它们互相引用,并且不再被别的对象所引用,那么它们应该被垃圾回收吗?
def func():
show_memory_info('initial')
a = [i for i in range(10000000)]
b = [i for i in range(10000000)]
show_memory_info('after a, b created')
a.append(b)
b.append(a)
func()
show_memory_info('finished')
########## 输出 ##########
initial memory used: 47.984375 MB
after a, b created memory used: 822.73828125 MB
finished memory used: 821.73046875 MB
复制代码
这里,a 和 b 互相引用,并且,作为局部变量,在函数 func 调用结束后,a 和 b 这两个指针从程序意义上已经不存在了。但是,很明显,依然有内存占用!为什么呢?因为互相引用,导致它们的引用数都不为 0。
试想一下,如果这段代码出现在生产环境中,哪怕 a 和 b 一开始占用的空间不是很大,但经过长时间运行后,Python 所占用的内存一定会变得越来越大,最终撑爆服务器,后果不堪设想。
当然,有人可能会说,互相引用还是很容易被发现的呀,问题不大。可是,更隐蔽的情况是出现一个引用环,在工程代码比较复杂的情况下,引用环还真不一定能被轻易发现。
那么,我们应该怎么做呢?
事实上,Python 本身能够处理这种情况,我们刚刚讲过的,可以显式调用 gc.collect() ,来启动垃圾回收。
import gc
def func():
show_memory_info('initial')
a = [i for i in range(10000000)]
b = [i for i in range(10000000)]
show_memory_info('after a, b created')
a.append(b)
b.append(a)
func()
gc.collect()
show_memory_info('finished')
########## 输出 ##########
initial memory used: 49.51171875 MB
after a, b created memory used: 824.1328125 MB
finished memory used: 49.98046875 MB
复制代码
调试内存泄漏
虽然有了自动回收机制,但这也不是万能的,难免还是会有漏网之鱼。内存泄漏是我们不想见到的,而且还会严重影响性能。有没有什么好的调试手段呢?
答案当然是肯定的,接下来我就为你介绍一个“怕孤单的鞋垫”。
它就是 objgraph,一个非常好用的可视化引用关系的包。在这个包中,我主要推荐两个函数,第一个是 show_refs(),它可以生成清晰的引用关系图。
通过下面这段代码和生成的引用调用图,你能非常直观地发现,有两个 list 互相引用,说明这里极有可能引起内存泄露。这样一来,再去代码层排查就容易多了。import objgraph
import objgraph
a = [1, 2, 3]
b = [4, 5, 6]
a.append(b)
b.append(a)
objgraph.show_refs([a])
复制代码