Python的垃圾回收


在介绍垃圾回收之前,我们先来看看什么是内存管理。

内存管理

在任意一种编程语言中,都是通过声明对象、操作对象来完成某项任务。这里的对象可以是简单类型,比如:字符串、整数等等。也可以是像数字、哈希、类等复杂的数据结构。这些对象的值保存在内存中,可以方便程序快速读取数据进行操作。 编程语言的内存管理是编程语言设计的一个重要方面。它是决定编程语言性能的重要因素。无论是C语言的手工管理,还是Java的垃圾回收,都成为编程语言最重要的特征。

手工年代

在使用早年的编程语言(C、C++等)开发时,开发人员需要自己在代码中对内存进行管理。也就是说,在声明一个对象之前需要先进行内存分配,分配完毕之后才能进行后续操作。同时,在对象不再使用时,需要手工调起free操作,将内存释放掉。

这样会带来2个问题:

  1. 忘记释放内存。 忘记释放内存会导致内存泄露,尤其是在对象被反复创建的场景下,内存占用会快速上涨,最终导致程序崩溃;
  2. 内存释放过早。这种情况是指对象还在使用,但是已经被释放掉了,这样也会带来意想不到的问题。

考虑到这样的情况,在新的编程语言中大多都会使用自动的内存管理机制了。

自动内存管理和GC

在自动内存管理机制下,运行时(runtime)取代程序员来进行内存管理。

引用计数

引用计数(reference counting)是目前来说最流行的一种内存管理机制。在使用引用计数的环境下,运行时(runtime)需要记录每一个对象的引用次数,当一个对象的引用次数为0时,运行时就认为这个对象可以被删除从而释放掉内存了。

这样的好处是程序员不再需要关注底层的内存使用细节,从而将更多的精力放在具体的功能实现上。与此同时,也避免了内存泄露或者野指针的情况。

但是,凡事都有两面性,自动内存管理带来方便的同时,也带来了2个问题:

  1. 更多的资源要求。运行时(runtime)需要额外的内存来对对象进行跟踪、记录。
  2. 『程序暂停』。比如在Java进行GC时,需要暂停所有正在进行的操作,这样会导致系统在某一时刻完全暂停(stop-the-world)
这就像早年的手动挡汽车和自动挡汽车一样。手动挡的乐趣在于完全由自己操控,而自动挡的乐趣在于不需要关心档位,只要安心驾驶即可。

随着摩尔定律的不断发展,机器的内存越来越大。因此自动内存管理带来的好处也越来越明显。因此目前大多数的流行语言(Java、Golang、Python等等)都使用了自动内存管理。 但是在一些需要长时间运行并且非常在意性能的场景下,还是建议使用手工内存管理。

现在我们大概了解了什么是内存管理和GC。接下来就来看看在Python中GC是怎么工作的。

Python如何实现GC

Python目前支持2种GC机制,分别是:

  1. 引用计数(Reference counting)
  2. 分代垃圾回收(Generational garbage collection)

引用计数 in Python

在CPython中,主要的GC机制就是引用计数。当你创建一个Python对象时,底层的C对象会同时包含一个Python类型(比如list、dict或者函数)以及一个引用计数。 在Python程序中,每增加一次对某个对象的引用,这个对象的引用技术就会加1。而当该对象减少了一次引用,那么它的引用计数也会相应减少。当该对象的引用计数为0时,这个对象就会被释放(可能不是立刻释放) 同时要注意,我们不能在代码中禁止引用计数。

引用计数非常好用,并且易于理解,但是当它面对一些复杂的问题(比如循环引用)时会表现的很无力。也因此有很多人说引用计数是『穷人的GC』,

示例

我们可以使用Python的sys模块来实时查看一个指定对象的引用计数。例如:

>>> import sys

>>> l = [1, 2, 3]
>>> print(sys.getrefcount(l))
2

程序的执行流程如下:

1. 程序定义变量[1,2,3],利用赋值语句,引用l只想对象[1, 2, 3],此时对象[1,2,3]的引用为1
2. 将l作为参数传递给sys.getrefcount(),因此对象[1,2,3]的引用次数+1
所以最终print出来的结果为2。

可以看到,引用计数可以很好的工作。但是,当面对这样的情况:

>>> class MyClass(object):
...     pass
...
>>> a = MyClass()
>>> a.obj = a
>>> del a

虽然调用del a来删除了a这个对象实例,但是Python并不会将a使用的内存释放掉。因为它的引用计数不是0。这种情况就是『循环引用』,面对这样的情况,就需要另外一个内存管理机制: 分代垃圾回收 上场了。

分代垃圾回收(Generational garbage collection)

分代垃圾回收机制有2个重要的概念:

  1. 代(generation)
  2. 阈值(threshold)
generation

垃圾收集器(garbage collector)会跟踪所有内存中的对象。一个新的对象首先会在第一代(first generation)的GC中。如果Python在这一代(generation)上执行了GC并且这个对象没有被回收释放时,这个对象就会被移动到下一代,也就是第二代(second generation),以此类推。Python的GC总共由3代(three geenrations)组成。一个对象如果没有被当前代的GC回收,那么就会被放到老一代(elder generation)的GC中。

threshold

而阈值(threshold)是指对于每一代的GC都会有一个最多对象数量的阈值。如果对象数量超过这个阈值,GC就会触发一次回收。以此来将对象一代代淘汰掉。

和引用计数(reference counting)机制不同的是,我们可以在Python程序中修改分代垃圾回收的行为。比如可以修改触发垃圾回收的阈值(thresholds),手动触发GC,或者禁止GC。

使用GC

我们可以通过gc模块来修改程序中的gc行为。例如:

>>> import gc
>>> gc.get_threshold()
(700, 10, 10)

默认情况下,Python的最年轻一代(youngest generation)GC的阈值为700,另外2代的阈值都为10。我们可以通过gc.get_count()查看当前每一代gc的对象数量:

>>> import gc
>>> print(gc.get_count())
(639, 4, 1)

可以看到,Python解释器在初始化时,就已经创建了大量的对象,因此第一代GC中已经有639个对象了。 这里,我们可以通过: gc.collect()来手工触发一次gc:

>>> gc.collect()
>>> print(gc.get_count())
(0, 0, 0)

结果显示所有的对象都被gc掉了。

此外,我们还可以通过gc.set_threshold来修改每一代gc的阈值:

>>> gc. set_threshold(1000, 15, 15)
>>> print(gc.get_threshold())
(1000, 15, 15)

使用GC的一些建议

尽量不要修改GC的默认行为

或者说,尽量不要考虑GC相关的问题,毕竟当你使用了Python这样的高级语言,还是应该将更多的精力放在应用层面,而不是底层GC(而且,GC只是性能优化的一小部分)上。

可以考虑禁用GC

这是一个工作中遇到的问题,当Python程序中声明了太多的对象时,可能会频繁触发GC,导致程序的性能严重下降。此时可以考虑禁止掉GC,通过内存换时间的方式来保证程序的稳定性和效率。