Skip to content
On this page

垃圾回收(GC)

Python中的垃圾回收(GC)是以引用计数为主,分代回收为辅。两种GC方式与C#类似,不同的是,Python的引用计数GC适时进行,C#则按策略不定时执行。

GC的工作除了垃圾回收,还负责对新对象分配内存。

1. 引用计数

Python里每一个东西都是对象,它们的核心就是一个结构体PyObject(CPython)。

c
typedef struct_object {
    int ob_refcnt;
    struct_typeobject *ob_type;
} PyObject;

其中ob_refcnt就是作引用计数。当对象有新的引用时,它的ob_refcnt就会增加,当引用它的对象被删除,它的ob_refcnt就会减少。当引用计数为0时,该对象就会被GC。

sys模块的sys.getrefcount(obj)可以获取对象的引用计数,由于该函数执行时会内部会定义变量接收对象,统计完即释放,所以测量引用数=实际引用数+1

py
import sys

a = [1, 2, 3]
b = a
print(sys.getrefcount(a))  # 3

以下行为引用计数+1:

  • 对象被创建,例如a=23
  • 对象被引用,例如b=a
  • 对象被作为参数,传入到一个函数中,例如func(a)
  • 对象作为一个元素,存储在容器中,例如list1=[a,a]

以下行为引用计数-1:

  • 对象的别名被显式销毁,例如del a
  • 对象的别名被赋予新的对象,例如a=24
  • 一个对象离开它的作用域,例如f函数执行完毕时,func函数中的局部变量(全局变量不会)
  • 对象所在的容器被销毁,或从容器中删除对象

优点

  • 机制简单高效。
  • 实时性强。一旦没有引用,内存立即释放了。

缺点

  • 维护引用计数消耗资源。
  • 无法解决循环引用问题。
py
# 循环引用示例
list1, list2 = [], []
list1.append(list2)
list2.append(list1)
del list1, list2

循环引用

  • list1=[]会首先申请一块内存假设地址为addr1,然后将list1指向addr1。同理假定list2指向addr2

  • list1.append(list2)会在list1内创建创建一个对象假定地址为addr3并指向list2地址即addr2。同理假定list2内创建对象地址addr4指向addr1。此时addr1addr2同时都有两个引用。

  • del list1会删除list1addr1的引用,同理list2addr2的引用也会被删除。此时addr1add2的引用计数都为1,所占用的内存永远无法被回收,随着出现此问题的变量累积最终将会导致内存泄露。

2. 分代回收

为了解决循环引用的问题,Python中辅助使用分代回收(Generational GC)机制,其与C#中GC的分代回收类似。

Python使用一种链表来持续追踪活跃的对象,Python的内部C代码将其称为零代(Generation Zero)。每次创建一个对象时Python会将其加入零代链表。

图中蓝色的箭头表示对象正在被零代链表之外的变量所引用。可以看到ABC和DEF节点包含的引用数为1且没有零代链之外的对象引用,说明它们在零代链表中现存对象中存在引用。

generation zero

随后,Python会循环遍历零代链表上的每个对象,检查链表中每个互相引用的对象。在这个过程中,Python会一个接一个的统计内部引用的数量以防过早地释放对象。

根据规则Python会尝试把循环引用的变量引用数减1,在下图中国可以看到ABC和DEF的引用计数已经变归零。这意味着收集器可以释放它们并回收内存空间了。剩下的活跃的对象则被移动到一个新的链表,一代链表。 generation one

零代链表的GC过程同样发生在一代链表中,经过一代链表GC后,剩下活跃的对象会被移动二代链表。代码长期使用的对象和持续访问的活跃对象,会从零代链表转移到一代再转移到二代。通过不同的阈值设置,Python可以在不同的时间间隔处理这些对象。Python处理零代最为频繁,其次是一代然后才是二代。回收高higher generation时也会清理lower generation,如回收二代时会回收一代和零代。

3. gc 模块

模块成员功能说明
collect([generation])显式进行垃圾回收。返回不可达(unreachable objects)对象的数目
garbage垃圾回收后的对象会放在此列表里中
get_threshold()获取的gc模块中自动执行垃圾回收的频率
set_threshold(threshold0[, threshold1[, threshold2])设置自动执行垃圾回收的频率
get_count()获取当前自动执行垃圾回收的计数器,返回一个长度为三个值组成的元组
set_debug(flags)设置gc的debug日志,一般设置为gc.DEBUG_LEAK

如果gc.get_count()返回(262, 3, 19)。其中262是指距离上次零代垃圾检查,Python分配内存的数目减去释放内存的数目。相当于上次GC后内存消耗。3是指距离上次一代垃圾检查,零代垃圾检查的次数,同理,19是指距离上一次二代垃圾检查,一代垃圾检查的次数。

gc模快有一个自动垃圾回收的阀值,即通过gc.get_threshold函数获取到的长度为3的元组,例如(700,10,10)每一次计数器的增加,gc模块就会检查增加后的计数是否达到阀值的数目,如果是,就会执行对应的代数的垃圾检查,然后重置计数器。例如,假设阀值是(700,10,10):

当计数器从(699,3,0)增加到(700,3,0),gc模块就会执行gc.collect(0),即检查零代对象的垃圾,并重置计数器为(0,4,0)
当计数器从(699,9,0)增加到(700,9,0),gc模块就会执行gc.collect(1),即检查零、一代对象的垃圾,并重置计数器为(0,0,1)
当计数器从(699,9,9)增加到(700,9,9),gc模块就会执行gc.collect(2),即检查零、一、二代对象的垃圾,并重置计数器为(0,0,0)

以下三种情况会触发GC:

  • 调用gc.collect()
  • 当gc模块的计数器达到阀值时
  • 程序退出时

gc模块唯一处理不了的是循环引用的类都有__del__方法,所以项目中要尽量避免定义__del__方法。

Released under the MIT License.