Python垃圾回收机制
计数引用
Python中一切皆对象,因此,所看到的一切变量,本质上都是对象的一个指针。
如何知道一个对象,是否永远都不能被调用了?
可以通过这个对象的引用计数(指针数)为0的时候,说明这个对象永不可达,自然它就称为了垃圾,需要被回收。
示例
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('{} memory used: {} MB'.format(hint, memory))
def func():
show_memory_info('initial')
a = [i for i in range(10000000)]
show_memory_info('after a created')
func()
show_memory_info('finished')
########## 输出 ##########
initial memory used: 47.19140625 MB
after a created memory used: 433.91015625 MB
finished memory used: 48.109375 MB
可以看到,调用函数func(),在列表a被创建之后,内存占用迅速增加了433MB;而在函数调用结束后,内存则返回正常。
这是因为,函数内部声明的列表a是局部变量,在函数返回后,局部变量的引用会注销掉;此时,列表a所指代对象的引用为0,Python便会执行垃圾回收,因此之前占用的大量内存就又回来了。
修改后的代码
def func():
show_memory_info('initial')
global a
a = [i for i in range(10000000)]
show_memory_info('after a created')
func()
show_memory_info('finished')
########## 输出 ##########
initial memory used: 48.88671875 MB
after a created memory used: 433.94921875 MB
finished memory used: 433.94921875 MB
新代码,global a表示将a声明为全局变量。那么,即使函数返回后,列表的引用依然存在,于是对象就不会被垃圾回收掉,依然占用大量内存。
同样,如果把生成的列表返回,然后再主程序中接受,那么引用依然存在,垃圾回收就不会被触发,大量内存依然被占用
def func():
show_memory_info('initial')
a = [i for i in derange(10000000)]
show_memory_info('after a created')
return 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()函数并不是统计一个指针,而是要统计一个对象被引用的次数。
手动释放内存
只需先调用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)
<ipython-input-12-153e15063d8a> in <module>
11
12 show_memory_info('finish')
---> 13 print(a)
NameError: name 'a' is not defined
循环引用
实例
- 如果有两个对象,它们互相引用,并且不再被别的对象所引用
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。
事实上,Python本身能够处理这种情况。
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
Python使用标记清除(mark-sweep)算法和分代收集(generational),来启用针对循环引用的自动垃圾回收。
标记清除算法
对于一个有向图,如果从一个节点出发进行遍历,并标记其经过的所有节点;那么,再遍历结束后,所有没有被标记的节点,称之为不可达节点。
显然,这些节点的存在是没有任何意义的,自然的,就需要对它们进行垃圾回收。
当然,每次都遍历全图,对于Python而言是一种巨大的性能浪费。所以,再Python的垃圾回收实现中,mark-sweep使用双向链表维护了一个数据结构,并且只考虑容器类的对象(只用容器类对象才有可能产生循环引用)。
分代收集算法
Python将所有对象分为三代。刚刚创立的对象是第0代;经过一次垃圾回收后,依然存在的对象,便会依次从上一代挪到下一代。而每一代启动自动垃圾回收的阈值,则是可以单独指定的。
当垃圾回收器中新增对象减去删除对象达到响应的阈值时,就会对这一代对象启动垃圾回收。
分代收集基于的思想是,新生的对象更优可能被垃圾回收,而存活更久的对象也有更高的概率继续存活。