除了通过引用计数直接销毁对象之外,python 还是拥有内在GC 机制的,并且也有完整的一套流程。
如果只有通过引用计数销毁对象这种机制,那么随便构造一个循环引用就会造成内存泄漏,比如下面的代码:
def _dump_gc():
gcobjs = gc.get_objects()
pprint.pprint(len(gcobjs))
def test_circle():
def _test_internal():
_dump_gc()
a = []
b = []
a.append(b)
b.append(a)
_dump_gc()
_test_internal()
_dump_gc()
gc.collect()
_dump_gc()
打印出来的结果是:
13707
13709
13709
13370
很显然,当退出_test_internal 作用域时,gc 对象的数量没有变化,这就说明在_test_internal 里创建的a、b 两个对象没有被立即释放掉。如果注释掉两个append 行,就能看到打印结果第三行变成了13707 ,说明a、b 在退出函数时就被销毁了。
所以首先,我们从循环引用入手,来研究一下python 的gc 机制(好巧不巧的是,python 的gc 也是专门为循环引用而设置的)。调用gc.collect 触发gc 之后,会跑到gc_collect_main 触发完整的gc 流程。gc_collect_main 是python 整个gc 流程的主入口,我们来看其中的代码:
static Py_ssize_t
gc_collect_main(PyThreadState *tstate, int generation,
Py_ssize_t *n_collected, Py_ssize_t *n_uncollectable,
int nofail)
{
int i;
Py_ssize_t m = 0;
Py_ssize_t n = 0;
PyGC_Head *young;
PyGC_Head *old;
PyGC_Head unreachable;
PyGC_Head finalizers;
PyGC_Head *gc;
_PyTime_t t1 = 0;
GCState *gcstate = &tstate->interp->gc;
if (generation+1 < NUM_GENERATIONS)
gcstate->generations[generation+1].count += 1;
for (i = 0; i <= generation; i++)
gcstate->generations[i].count = 0;
for (i = 0; i < generation; i++) {
gc_list_merge(GEN_HEAD(gcstate, i), GEN_HEAD(gcstate, generation));
}
young = GEN_HEAD(gcstate, generation);
if (generation < NUM_GENERATIONS-1)
old = GEN_HEAD(gcstate, generation+1);
else
old = young;
deduce_unreachable(young, &unreachable);
untrack_tuples(young);
if (young != old) {
if (generation == NUM_GENERATIONS - 2) {
gcstate->long_lived_pending += gc_list_size(young);
}
gc_list_merge(young, old);
}
else {
untrack_dicts(young);
gcstate->long_lived_pending = 0;
gcstate->long_lived_total = gc_list_size(young);
}
gc_list_init(&finalizers);
move_legacy_finalizers(&unreachable, &finalizers);
move_legacy_finalizer_reachable(&finalizers);
m += handle_weakrefs(&unreachable, old);
validate_list(old, collecting_clear_unreachable_clear);
validate_list(&unreachable, collecting_set_unreachable_clear);
finalize_garbage(tstate, &unreachable);
PyGC_Head final_unreachable;
handle_resurrected_objects(&unreachable, &final_unreachable, old);
m += gc_list_size(&final_unreachable);
delete_garbage(tstate, gcstate, &final_unreachable, old);
handle_legacy_finalizers(tstate, gcstate, &finalizers, old);
if (generation == NUM_GENERATIONS-1) {
clear_freelists(tstate->interp);
}
if (_PyErr_Occurred(tstate)) {
if (nofail) {
_PyErr_Clear(tstate);
}
else {
_PyErr_WriteUnraisableMsg("in garbage collection", NULL);
}
}
if (n_collected) {
*n_collected = m;
}
if (n_uncollectable) {
*n_uncollectable = n;
}
struct gc_generation_stats *stats = &gcstate->generation_stats[generation];
stats->collections++;
stats->collected += m;
stats->uncollectable += n;
assert(!_PyErr_Occurred(tstate));
return n + m;
}
gc_collect_main 总共执行了以下几个步骤,一个一个来说。
首先更新指定generation 后的分配计数(+1),指定generation 及以下的全部归0。这是因为python 也自带了分代回收机制,gc.collect 入参是generation ,指定generation 以下的全部都会被gc 掉。
python 的gc 分代总共是3代,而gc.collect 默认值是2,也表示最高一代,通俗点讲就是Full GC 。每一代的对象数目如果超过特定值,就会触发自动gc。
然后做的一个事情是,将指定generation 前代的和当代的所有GC_Head 全部串到一个链表上,这样只需要处理这一个链表就能回收所有东西。每个python 对象都会自带GC_Head ,串到特定genenration 的链表中,用于在GC时候被识别到。
之后做的很关键的一步是deduce_unreachable ,其作用是模拟去引用的流程,探测无法从gc 根对象直接达到的对象,放到单独的unreachable 列表中。并且在这一步会通过遍历对象之间的引用关系并-1引用的方式,从而将对象之间的循环引用暂时归零。deduce_unreachable 的代码如下:
static inline void
deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable) {
validate_list(base, collecting_clear_unreachable_clear);
update_refs(base);
subtract_refs(base);
gc_list_init(unreachable);
move_unreachable(base, unreachable);
validate_list(base, collecting_clear_unreachable_clear);
validate_list(unreachable, collecting_set_unreachable_set);
}
好比一个场景:总共有4个list 对象,分别为l1 到l4 ,其中l1 、l2 循环引用,l3 、l4 循环引用,l1 和l2 与l3 跟l4 之间没有关联。之后若再有一个变量a 引用了l1 ,那么经过deduce_unreachable 之后,会呈现如下的结果:
- 在
subtract_refs 步骤中,l1 的引用数目变成1,l2 、l3 、l4 变成0 - 在
move_unreachable 步骤中,发现l1 引用数目为1,即将l1 以及其引用到的所有变量标记为reachable
- 最后剩下来引用数仍然为0的
l3 、l4 ,放到unreachable 链表中
经历deduce_unreachable 步骤存活下来的reachable 对象,会直接被移动到下一个generation 。接下来只需要考虑对unreachable 的对象进行销毁了。
接下来的操作是将unreachable 链表中,会处理一些C 层类型定义里含有旧版tc_del 方法的类型的对象,这些对象及其直接或间接引用的对象全部都会被移动到单独的finalizers 链表中,而这个链表中的对象会被单独维护,无法被回收。
在以前的版本中,如果python 类型定义了__del__ 方法,那么这些类型的对象就会移动到finalizers 链表中。直到PEP442之后,__del__ 方法就对应了C 层类型定义的另外一个tc_finalize 方法了,因此包含__del__ 方法的类型的对象,不一定会移动到finalizers 链表中,而是会在后面的步骤中触发tc_finalize 逻辑。
再之后,针对剩下的unreachable 对象,通过handle_weakrefs 方法解除其它对象对其的弱引用。然后调用finalize_garbag e销毁unreachable 对象。finalize_garbage 的代码如下:
static void
finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable)
{
destructor finalize;
PyGC_Head seen;
gc_list_init(&seen);
while (!gc_list_is_empty(collectable)) {
PyGC_Head *gc = GC_NEXT(collectable);
PyObject *op = FROM_GC(gc);
gc_list_move(gc, &seen);
if (!_PyGCHead_FINALIZED(gc) &&
(finalize = Py_TYPE(op)->tp_finalize) != NULL) {
_PyGCHead_SET_FINALIZED(gc);
Py_INCREF(op);
finalize(op);
assert(!_PyErr_Occurred(tstate));
Py_DECREF(op);
}
}
gc_list_merge(&seen, collectable);
}
finalize_garbage 实质是调用对象类型定义的tp_finalize 方法析构对应的对象,并减少其引用为0从而释放对象内存。由于某些对象类型可能没有默认的tp_finalize 方法,因此经过这一步骤之后,还会存留一些未销毁的对象。
针对未销毁的对象,之后会通过handle_resurrected_objects 进行处理。在handle_resurrected_objects 中会再一次执行deduce_unreachable 模拟去引用操作,存活下来的unreachable 对象就被移到下一个generation ,而剩下的对象就会被移动到final_unreachable 链表进行后面的销毁操作。
通过delete_garbage 方法会对final_unreachable 链表的对象进行销毁,其代码如下:
static void
delete_garbage(PyThreadState *tstate, GCState *gcstate,
PyGC_Head *collectable, PyGC_Head *old)
{
assert(!_PyErr_Occurred(tstate));
while (!gc_list_is_empty(collectable)) {
PyGC_Head *gc = GC_NEXT(collectable);
PyObject *op = FROM_GC(gc);
_PyObject_ASSERT_WITH_MSG(op, Py_REFCNT(op) > 0,
"refcount is too small");
if (gcstate->debug & DEBUG_SAVEALL) {
assert(gcstate->garbage != NULL);
if (PyList_Append(gcstate->garbage, op) < 0) {
_PyErr_Clear(tstate);
}
}
else {
inquiry clear;
if ((clear = Py_TYPE(op)->tp_clear) != NULL) {
Py_INCREF(op);
(void) clear(op);
if (_PyErr_Occurred(tstate)) {
_PyErr_WriteUnraisableMsg("in tp_clear of",
(PyObject*)Py_TYPE(op));
}
Py_DECREF(op);
}
}
if (GC_NEXT(collectable) == gc) {
gc_clear_collecting(gc);
gc_list_move(gc, old);
}
}
}
delete_garbage 中,每一个对象都会调用其类型的tp_clear 方法,减少对象引用数目为0,触发对象的销毁。
在delete_garbage 之后,终于就会对先前单独拎出来的finalizers 链表进行处理。finalizers 链表中所有的内容都会通过handle_legacy_finalizers 方法被移动到当前gcstate 的garbage 链表中单独维护,不会被销毁。
之后进行一些数据清理和数据统计逻辑,整个gc 流程就完成了。
|