Posts Python-垃圾回收
Post
Cancel

Python-垃圾回收

前言

垃圾回收对于每一门语言来说都是至关重要的,其核心功能是进行“内存管理”,防止”内存泄漏“,所谓”内存泄漏”指的是:程序未能释放已不再使用的内存,通常是因为程序设计上的问题,导致程序失去了对使用过的内存的控制,导致了内存的浪费。 本文重点梳理下 Python 下的垃圾回收机制。

垃圾回收方法

Python 中最常用的是 计数引用 的机制: Python 中一切都是对象,所有的变量都是对象的引用。因此,最直观的想法,便是通过对象的引用计数来进行垃圾回收: 当一个对象的引用计数为0时,说明这个对象不可达,需要被回收 简单用例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

Python 也可以采用手动进行垃圾回收: del a + gc.collect() ,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

当然,在 Python 中,引用次数为0不是垃圾回收启动的充要条件,原因是: 循环引用 。即函数内,a和b如果互相引用,当函数调用结束后,a/b从程序意义上看,不存在意义,应当被消除,但是实际上这种情况下,内存依然占用,需要手动释放:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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()
show_memory_info('before collected')
gc.collect()
show_memory_info('finished')

########## 输出 ##########

initial memory used: 49.51171875 MB
after a, b created memory used: 824.1328125 MB
before collected memory used: 824.1328125 MB
finished memory used: 49.98046875 MB

显式调用 gc.collect() 可以清理内存,可以看到 Python 下垃圾回收机制除了“引用计数”外还存在其他方式:

  • 标记清除(mark-sweep) 标记清除采用的是算法是:单节点出发进行遍历,标记所有可达的节点;遍历结束后,得到所有未标记的节点,作为“不可达节点”。最后,对这些“不可达节点”进行垃圾回收即可。具体实现层面,”Python“采用了一个双链表来实现,且只考虑容器类的对象(只有该对象才存在循环引用)。
  • 分代收集(generational) 分代收集在于优化,核心是优化垃圾回收手段。其实现方式:将对象分为三代:刚创立的为0代,每经过一代,则将当代仍然存在的对象移动到下一代。而每代对象的垃圾回收阈值是单独指定的。当来垃圾回收器中新增对象减去删除对象达到相应的阈值时,就会对这代对象启动垃圾回收。 其设计出发点是:新生的对象更有可能被垃圾回收,而存活越久的对象也有更高的概率继续存活。该方式能够节约计算量,提高Python的性能。

内存泄漏调试

介绍下调试内存泄漏的工具:objgraph 方便可视化引用关系的包: show_refs() 可以生成清晰的引用关系图。如下文,可以看到清晰的互用引用关系图:

1
2
3
4
5
6
7
8
9
10
import objgraph

a = [1, 2, 3]
b = [4, 5, 6]

a.append(b)
b.append(a)

objgraph.show_refs([a])

img

show_backrefs() 同样可以生成前后引用关系图:

1
2
3
4
5
6
7
8
9
import objgraph

a = [1, 2, 3]
b = [4, 5, 6]

a.append(b)
b.append(a)

objgraph.show_backrefs([a])

img

This post is licensed under CC BY 4.0 by the author.

Python-闭包+装饰器

Python-上下文管理器