LL) {
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) {
/* object is still alive, move it, it may die later */
gc_clear_collecting(gc);
gc_list_move(gc, old);
}
}
}
其中的逻辑也简单,遍历最终不可达列表,然后调用每个对象的tp_clear函数。调用后,如果对象可以被释放,则也会从GC列表中移除。所以在后面有一个判断if (GC_NEXT(collectable) == gc),也就是该对象还没有被移除,这种情况则清除该对象的收集中标记,然后移入老年代中。
- 将finalizers列表中的对象移入老年代中
static void
handle_legacy_finalizers(PyThreadState *tstate,
GCState *gcstate,
PyGC_Head *finalizers, PyGC_Head *old)
{
assert(!_PyErr_Occurred(tstate));
assert(gcstate->garbage != NULL);
PyGC_Head *gc = GC_NEXT(finalizers);
for (; gc != finalizers; gc = GC_NEXT(gc)) {
PyObject *op = FROM_GC(gc);
if ((gcstate->debug & DEBUG_SAVEALL) || has_legacy_finalizer(op)) {
if (PyList_Append(gcstate->garbage, op) < 0) {
_PyErr_Clear(tstate);
break;
}
}
}
gc_list_merge(finalizers, old);
}
所以说,定义了__del__的对象,有可能出现无法回收的情况。需要仔细编码。
总结
python的垃圾回收主要用到了
- 引用计数
- 标记清除
- 分代回收
其中分代回收步骤为
- 将年轻代的对象移动到指定回收代的列表后。
- 遍历回收代列表,将对象设置为收集中PREV_MASK_COLLECTING标记,然后将引用计数复制一份到_gc_prev中
- 然后遍历每个对象中的每个元素,如果这个元素也是可GC对象,并且也有收集中标记,则将_gc_prev中的计数值减1
- 再遍历回收代列表,判断_gc_prev计数值是否为0,
- 如果为0,则标记为不可达,然后移动到不可达列表中。
- 如果不为0,则遍历该对象的元素,如果该元素已经标记为清除,就把该元素移动到原回收代列表中。(也就是父对象仍然可达,则子对象也可达)。然后清除该对象的收集中标记。
- 遍历不可达列表,清除不可达标记,判断是否定义了__del__函数,如果有,则将清除收集中标记,并移入finalizers列表中。
- 遍历finalizers列表的每个对象,判断对象中的元素是否是可GC对象,并且有收集中标记,将该元素清除收集中标记,移入finalizers列表中。
- 遍历不可达列表, 处理弱引用
- 遍历不可达列表的每个对象,调用对象的tp_finalize函数,如果没有则跳过。
- 遍历不可达列表,将复活对象移到老年代列表中,其他对象移动到仍然不可达列表final_unreachable
- 最后遍历 final_unreachable 列表,为每个对象调用tp_clear函数
- 如果真的可以删除,则把自己从对应GC列表中摘除
- 如果还不能删除,则清除对象的收集中标记,对象重新加入老年代中。
- 将finalizers列表中的每个对象重新加入老年代列表中。
例子
说到这里好像还没有具体分析环引用的情况
import sys
import gc
def a():
lst1 = []
lst2 = []
lst1.append(lst2)
lst2.append(lst1)
print("lst1 refcnt: {}".format(sys.getrefcount(lst1)))
print("lst2 refcnt: {}".format(sys.getrefcount(lst2)))
before_collect_cnt = gc.collect(2)
a()
after_collect_cnt = gc.collect(2)
print("before({}), after({})".format(before_collect_cnt, after_collect_cnt))
在笔者的电脑上输出
hejs@ubuntu:~$ python main.py
lst1 refcnt: 3
lst2 refcnt: 3
before(0), after(2)
可以看到,在执行a函数时,lst1和lst2的引用计数为2(因为sys.getrefcount也会引用一次,所以输出的值是真实计数+1)。
当a函数调用结束后,由于函数内的lst1、lst2变量解除了引用,所以此时两个列表的计数值就为1了。出现环引用,无法释放。
这个时候就轮到标记清楚和分代回收解决了。
- 首先会将第0、1代的元素移到第2代上。因为gc.collect(2)
- 然后遍历第2代列表,为每个对象设置收集中标记,将对象的真实计数复制到_gc_prev中。
- 再遍历第2代列表,判断对象的子元素是否也是 可GC对象、也有收集中标记,如果有则将该元素计数值减1。
- 此时 lst1、lst2的_gc_prev计数值都为0
- 然后将_gc_prev计数值为0的对象移入不可达列表中。
- 因为listobject没有__del__函数,也没有tp_finalize函数,所以直接到第10步,调用tp_clear函数。
static int _list_clear(PyListObject *a)
{
Py_ssize_t i;
PyObject **item = a->ob_item;
if (item != NULL) {
i = Py_SIZE(a);
Py_SET_SIZE(a, 0);
a->ob_item = NULL;
a->allocated = 0;
while (--i >= 0) {
Py_XDECREF(item[i]);
}
PyMem_FREE(item);
}
/* Never fails; the return value can be ignored.
Note that there is no guarantee that the list is actually empty
at this point, because XDECREF may have populated it again! */
return 0;
}
也就是会为每个元素的引用计数减1。从之前分析可知,当计数减为0时,会调用对象的tp_dealloc函数,再看看lis