1. 引言
当Python程序在运行的时候,需要在内存中开辟出一块空间,用来存放运行时产生的临时变量;计算完成后,再将结果输出到永久性存储器中。如果数据量过大,内存空间就有可能会出现OOM(out of memory),程序可能被操作系统终止。
泄漏指的是:程序本身没有设计好,导致程序本身未能释放已经不再使用的内存
内存泄漏指的是:在代码分配某段内存后,因为设计错误,失去了对这段内存的控制,从而造成的内存的浪费。
我们只需要记住最关键的一句话:
Python中的垃圾回收机制是以引用计数为主,标记-清除和分代回收两种机制为辅的策略
2. 引用计数
Python中一切皆是对象,所以我们看到的所有的变量,本质上都是对象的一个指针。
那么,我们怎么判断这个对象是否需要被回收呢?
当这个对象的引用计数(指针数)为0,那么就意味着没有对于这个对象的引用,自然这个对象就成了垃圾,需要被回收。
2.1 例1:a为局部变量
import os
# 一个开源的获取系统信息的库
import psutil
# 显示Python程序占用的内存大小
def show_memory_info(hint):
pid = os.getpid()
p =psutil.Process(pid)
info = p.memory_full_info()
memory = info.uss / 1024. / 1024
print('{} memroy used:{} MB'.format(hint,memory))
def func():
show_memory_info('initial')
a = [i for i in range(10000000)]
show_memory_info('after created ')
func()
show_memory_info('finished')
# 输出
initial memroy used:6.1796875 MB
after created memroy used:398.26953125 MB
finished memroy used:10.515625 MB
在调用func()
后,在列表a创建后,内存占用达到了近400MB,而在函数调用结束后,内存则恢复到了之前的水平。
函数内部声明的列表a是局部变量,在函数返回后,局部变量的引用就会注销,此时,列表a所指代对象的引用数为0,Python便会执行垃圾回收,因此之前占用的大量内存就被释放了。
2.2 例2:a为全局变量
那么,我们将a声明为全局变量会发生什么呢?
import os
# 一个开源的获取系统信息的库
import psutil
# 显示Python程序占用的内存大小
def show_memory_info(hint):
pid = os.getpid()
p =psutil.Process(pid)
info = p.memory_full_info()
memory = info.uss / 1024. / 1024
print('{} memroy used:{} MB'.format(hint,memory))
def func():
show_memory_info('initial')
global a # 声明全局变量a
a = [i for i in range(10000000)]
show_memory_info('after created ')
func()
show_memory_info('finished')
# 输出
initial memroy used:6.16796875 MB
after created memroy used:398.28515625 MB
finished memroy used:398.28515625 MB
在我们声明a为全局变量后,即使函数返回后,列表的引用依然存在,此时Python的垃圾回收机制就不会回收a,依然占用内存。
2.3 例子3:a作为返回值
import os
# 一个开源的获取系统信息的库
import psutil
# 显示Python程序占用的内存大小
def show_memory_info(hint):
pid = os.getpid()
p =psutil.Process(pid)
info = p.memory_full_info()
memory = info.uss / 1024. / 1024
print('{} memroy used:{} MB'.format(hint,memory))
def func():
show_memory_info('initial')
a = [i for i in range(10000000)]
show_memory_info('after created ')
return a
a = func()
show_memory_info('finished')
# 输出
initial memroy used:6.1640625 MB
after created memroy used:398.2578125 MB
finished memroy used:398.2578125 MB
a作为返回值的话,因为在a=fun()
中引用了a,所以a还是不会被垃圾回收机制回收,内存仍然被占用着。
2.4 引用计数原理
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 = []
# 两次引用计数,一次是a,另一次是sys.getrefcount()
print('引用计数为:{} 次'.format(sys.getrefcount(a)))
b = a
# 三次 引用计数,前两次 + b=a的一次引用,sys.getrefcount()重复只计算一次
print('引用计数为:{} 次'.format(sys.getrefcount(a)))
c = b # 四次
d = b # 五次
e = b # 六次
f = b # 七次
g = d # 八次,通过 d 引用了 b
print('引用计数为:{} 次'.format(sys.getrefcount(a)))
2.5 手动垃圾回收
Python中垃圾回收相比于C语言中的free
来释放内存,简单了很多,但是如果我们需要手动释放内存呢?该如何操作呢?
那么,我们就需要先调用del a
来删除一个对象,然后调用gc.collect()
启动垃圾回收,代码如下:
import gc
import os
import psutil
def show_memory_info(hint):
pid = os.getpid()
p = psutil.Process(pid)
info = p.memory_full_info()
memory = info.uss / 1024. / 1024
print('{} memroy used:{} MB'.format(hint, memory))
show_memory_info('initial')
a = [i for i in range(10000000)]
show_memory_info('after created')
del a
gc.collect()
show_memory_info('finished')
print(a)
# 输出
initial memroy used:6.16015625 MB
after created memroy used:398.21875 MB
finished memroy used:10.46875 MB
Traceback (most recent call last):
File "/Users/gray/Desktop/test.py", line 18, in <module>
print(a)
NameError: name 'a' is not defined
可见,这就是Python的手动回收机制,经过手动回收,内存空间被回收了,列表a
也被删除,所以报错显示a
没有定义。
3. 循环引用
我们知道了引用计数为0,Python就会回收,那么如果两个对象互相引用,那么它们会被垃圾回收吗?
我们从一个例子入手:
import gc
import os
import psutil
def show_memory_info(hint):
pid = os.getpid()
p = psutil.Process(pid)
info = p.memory_full_info()
memory = info.uss / 1024. / 1024
print('{} memroy used:{} MB'.format(hint, memory))
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 memroy used:6.21484375 MB
after a,b created memroy used:660.42578125 MB
finished memroy used:660.15625 MB
很明显,由于a和b之间的互相引用,即使a,b均为局部变量,在函数调用结束之后,a和b的指针在程序意义上都不存在了,但是内存依然占用。
在这样简单的代码中,我们还是能够发现循环引用的,但是当工程代码复杂后,引用环不一定会容易被轻易发现。
循环引用这种情况,Python也是可以处理的,我们还是调用gc.collect()
来手动启动垃圾回收。
import gc
import os
import psutil
def show_memory_info(hint):
pid = os.getpid()
p = psutil.Process(pid)
info = p.memory_full_info()
memory = info.uss / 1024. / 1024
print('{} memroy used:{} MB'.format(hint, memory))
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 memroy used:6.15234375 MB
after a,b created memroy used:784.7109375 MB
finished memroy used:10.74609375 MB
手动垃圾回收生效了,Python中的垃圾回收并没有那么弱。
4. Python的垃圾回收
针对循环引用,Python中有专门的标记-清除和分代回收来处理。
4.1 标记-清除
对于一个有向图,如果从一个节点出发进行遍历,并标记其经过的所有节点;那么在遍历结束后,所有没有被标记的节点,我们就称之为不可达节点;显而易见,这些节点的存在是没有任何意义的,自然的,我们就需要对它们进行垃圾回收。
当然,每次都遍历全图,对于Python来说也是一种巨大的性能浪费,所以Python中的垃圾回收实现中,mark-sweep
使用双向链表维护了一个数据结构,并且只考虑容器类的对象(只有容器类对象才能产生循环引用)。
4.2 分代回收
Python将所有对象分为三代。刚刚创建的对象为0代;经历一次垃圾回收后,依然存在的对象,便会依次从上一代挪到下一代;每一代启动垃圾回收的阈值,是可以单独设定的。当垃圾回收容器中新增对象减去删除对象达到相应的阈值的时候,就会对这一代对象启动垃圾回收。
分代回收的思想其实是:新生代对象更有可能会被回收,而存活更久的对象也有更高的概率继续存活,通过这样的思路,可以节省不少计算量,从而提高Python的性能。
Q.E.D.