垃圾回收(GC)
Python中的垃圾回收(GC)是以引用计数为主,分代回收为辅。两种GC方式与C#类似,不同的是,Python的引用计数GC适时进行,C#则按策略不定时执行。
GC的工作除了垃圾回收,还负责对新对象分配内存。
1. 引用计数
Python里每一个东西都是对象,它们的核心就是一个结构体PyObject
(CPython)。
typedef struct_object {
int ob_refcnt;
struct_typeobject *ob_type;
} PyObject;
其中ob_refcnt
就是作引用计数。当对象有新的引用时,它的ob_refcnt
就会增加,当引用它的对象被删除,它的ob_refcnt
就会减少。当引用计数为0时,该对象就会被GC。
sys
模块的sys.getrefcount(obj)
可以获取对象的引用计数,由于该函数执行时会内部会定义变量接收对象,统计完即释放,所以测量引用数=实际引用数+1
。
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函数中的局部变量(全局变量不会)
- 对象所在的容器被销毁,或从容器中删除对象
优点
- 机制简单高效。
- 实时性强。一旦没有引用,内存立即释放了。
缺点
- 维护引用计数消耗资源。
- 无法解决循环引用问题。
# 循环引用示例
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
。此时addr1
和addr2
同时都有两个引用。del list1
会删除list1
对addr1
的引用,同理list2
对addr2
的引用也会被删除。此时addr1
与add2
的引用计数都为1,所占用的内存永远无法被回收,随着出现此问题的变量累积最终将会导致内存泄露。
2. 分代回收
为了解决循环引用的问题,Python中辅助使用分代回收(Generational GC)机制,其与C#中GC的分代回收类似。
Python使用一种链表来持续追踪活跃的对象,Python的内部C代码将其称为零代(Generation Zero)。每次创建一个对象时Python会将其加入零代链表。
图中蓝色的箭头表示对象正在被零代链表之外的变量所引用。可以看到ABC和DEF节点包含的引用数为1且没有零代链之外的对象引用,说明它们在零代链表中现存对象中存在引用。
随后,Python会循环遍历零代链表上的每个对象,检查链表中每个互相引用的对象。在这个过程中,Python会一个接一个的统计内部引用的数量以防过早地释放对象。
根据规则Python会尝试把循环引用的变量引用数减1,在下图中国可以看到ABC和DEF的引用计数已经变归零。这意味着收集器可以释放它们并回收内存空间了。剩下的活跃的对象则被移动到一个新的链表,一代链表。
零代链表的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__
方法。