一文详解Python垃圾回收
Python版本
v3.9.17
分析代码的过程比较枯燥,可以直接跳转到总结。
只能被其他对象引用类型
比如:longobject、floatobject
floatobject
以floatobject为例子来分析,先看看结构定义
typedef struct { PyObject_HEAD double ob_fval; } PyFloatObject; // 展开PyObject_HEAD后 typedef struct { PyObject ob_base; double ob_fval; } PyFloatObject; typedef struct _object { _PyObject_HEAD_EXTRA Py_ssize_t ob_refcnt; PyTypeObject *ob_type; } PyObject;
在PyObject中的_PyObject_HEAD_EXTRA
,只有在编译时指定--with-trace-refs
才有效,这里忽略即可。
./configure --with-trace-refs
可以看到在PyObject里有一个ob_refcnt
的属性,这个就是引用计数。
当对引用计数减为0时,就会调用各类型对应的析构函数。
define Py_DECREF(op) _Py_DECREF(_PyObject_CAST(op)) void _Py_Dealloc(PyObject *op) { destructor dealloc = Py_TYPE(op)->tp_dealloc; (*dealloc)(op); } static inline void _Py_DECREF(PyObject *op) { if (--op->ob_refcnt != 0) { } else { _Py_Dealloc(op); } }
能引用其他对象的类型
比如listobject,dictobject...
listobject
以listobject为例子来分析,先看看结构定义
typedef struct { PyObject_VAR_HEAD PyObject **ob_item; Py_ssize_t allocated; } PyListObject; // 展开 PyObject_VAR_HEAD typedef struct { PyVarObject ob_base; PyObject **ob_item; Py_ssize_t allocated; } PyListObject; typedef struct { PyObject ob_base; Py_ssize_t ob_size; /* Number of items in variable part */ } PyVarObject;
可以看出,PyObject_VAR_HEAD
也就比PyObject_HEAD
多了一个Py_ssize_t ob_size
而已,这个属性是用来表示这个可变对象里元素数量。
因为可以引用其他对象,就有可能会出现环引用问题,这种问题如果再使用引用计数来作为GC就会出现问题。
lst1 = [] lst2 = [] lst1.append(lst2) lst2.append(lst1)
当然这种情况可以使用弱引用,或者手动解除环引用。这些解决方案这里不深入,现在主要看看python是怎样应对这种情况。
对于这类型的对象在申请内存的时候调用的是PyObject_GC_New
,而不可变类型是用PyObject_MALLOC
。为了减少篇幅,删掉了一些判断逻辑。
typedef struct { // Pointer to next object in the list. // 0 means the object is not tracked uintptr_t _gc_next; // Pointer to previous object in the list. // Lowest two bits are used for flags documented later. uintptr_t _gc_prev; } PyGC_Head; #define FROM_GC(g) ((PyObject *)(((PyGC_Head *)g)+1)) static PyObject * _PyObject_GC_Alloc(int use_calloc, size_t basicsize) { PyThreadState *tstate = _PyThreadState_GET(); GCState *gcstate = &tstate->interp->gc; size_t size = sizeof(PyGC_Head) + basicsize; PyGC_Head *g; g = (PyGC_Head *)PyObject_Malloc(size); g->_gc_next = 0; g->_gc_prev = 0; gcstate->generations[0].count++; /* number of allocated GC objects */ if (/* 判断是否可以执行GC */) { gcstate->collecting = 1; collect_generations(tstate); gcstate->collecting = 0; } PyObject *op = FROM_GC(g); return op; }
在可变对象中,python又加上了一个PyGC_Head
。通过这个PyGC_Head
将listobject链接到gc列表中。
在分配完listobject内存后,紧接着调用_PyObject_GC_TRACK
,链接到gc列表中。
static inline void _PyObject_GC_TRACK_impl(const char *filename, int lineno, PyObject *op) { PyGC_Head *gc = _Py_AS_GC(op); PyThreadState *tstate = _PyThreadState_GET(); PyGC_Head *generation0 = tstate->interp->gc.generation0; PyGC_Head *last = (PyGC_Head*)(generation0->_gc_prev); _PyGCHead_SET_NEXT(last, gc); _PyGCHead_SET_PREV(gc, last); _PyGCHead_SET_NEXT(gc, generation0); generation0->_gc_prev = (uintptr_t)gc; }
通过这里的变量名,可以猜测使用到了分代垃圾回收。
分代回收
python手动执行垃圾回收一般调用gc.collect(generation=2)函数。
#define NUM_GENERATIONS 3 #define GC_COLLECT_METHODDEF \ {"collect", (PyCFunction)(void(*)(void))gc_collect, METH_FASTCALL|METH_KEYWORDS, gc_collect__doc__}, static PyObject * gc_collect(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) { PyObject *return_value = NULL; int generation = NUM_GENERATIONS - 1; Py_ssize_t _return_value; _return_value = gc_collect_impl(module, generation); if ((_return_value == -1) && PyErr_Occurred()) { goto exit; } return_value = PyLong_FromSsize_t(_return_value); exit: return return_value; }
具体执行在gc_collect_impl
函数中,接着往下
static Py_ssize_t gc_collect_impl(PyObject *module, int generation) { PyThreadState *tstate = _PyThreadState_GET(); GCState *gcstate = &tstate->interp->gc; Py_ssize_t n; if (gcstate->collecting) { /* already collecting, don't do anything */ n = 0; } else { gcstate->collecting = 1; n = collect_with_callback(tstate, generation); gcstate->collecting = 0; } return n; }
可以看到,如果已经在执行GC,则直接返回。接着看collect_with_callback
static Py_ssize_t collect_with_callback(PyThreadState *tstate, int generation) { assert(!_PyErr_Occurred(tstate)); Py_ssize_t result, collected, uncollectable; invoke_gc_callback(tstate, "start", generation, 0, 0); result = collect(tstate, generation, &collected, &uncollectable, 0); invoke_gc_callback(tstate, "stop", generation, collected, uncollectable); assert(!_PyErr_Occurred(tstate)); return result; }
其中invoke_gc_callback
是调用通过gc.callbacks
注册的回调函数,这里我们忽略,重点分析collect
函数。
collect函数签名
这段代码很长,我们拆分开来分析,这里会去除掉一些DEBUG相关的逻辑。
static Py_ssize_t collect(PyThreadState *tstate, int generation,Py_ssize_t *n_collected, Py_ssize_t *n_uncollectable, int nofail);
- 将新生代的对象合并到指定代的对象列表中。
/* merge younger generations with one we are currently collecting */ for (i = 0; i < generation; i++) { gc_list_merge(GEN_HEAD(gcstate, i), GEN_HEAD(gcstate, generation)); }
比如调用gc.collect(2)
,就表示启动全部的垃圾回收。这里就会将第0、1代的对象合并到第2代上。合并之后第0、1代上就空了,全部可GC的对象都在第2代上。
- 推断不可达对象
/* handy references */ young = GEN_HEAD(gcstate, generation); if (generation < NUM_GENERATIONS-1) old = GEN_HEAD(gcstate, generation+1); else old = young; validate_list(old, collecting_clear_unreachable_clear); deduce_unreachable(young, &unreachable);
这里的young指针指向第2代的链表头,validate_list
做校验,这里忽略,重点在deduce_unreachable
函数中。
static inline void deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable) { validate_list(base, collecting_clear_unreachable_clear); update_refs(base); // gc_prev is used for gc_refs subtract_refs(base); gc_list_init(unreachable); move_unreachable(base, unreachable); // gc_prev is pointer again validate_list(base, collecting_clear_unreachable_clear); validate_list(unreachable, collecting_set_unreachable_set); }
首先调用update_refs更新引用计数
static inline void gc_reset_refs(PyGC_Head *g, Py_ssize_t refs) { g->_gc_prev = (g->_gc_prev & _PyGC_PREV_MASK_FINALIZED) | PREV_MASK_COLLECTING | ((uintptr_t)(refs) << _PyGC_PREV_SHIFT); } static void update_refs(PyGC_Head *containers) { PyGC_Head *gc = GC_NEXT(containers); for (; gc != containers; gc = GC_NEXT(gc)) { gc_reset_refs(gc, Py_REFCNT(FROM_GC(gc))); _PyObject_ASSERT(FROM_GC(gc), gc_get_refs(gc) != 0); } }
这里的逻辑就是遍历所有对象,然后赋值_gc_prev,设置为收集中的标识PREV_MASK_COLLECTING,然后将引用计数赋值给_gc_prev 。最后_gc_prev的内容如下。
更新完_gc_prev后,就开始调用subtrace_refs,遍历对象中的元素,判断元素是否也是可GC对象并且有收集中标记,如果是则减去该对象的计数。注意这里减去的是_gc_prev中的计数,而不是真正的计数ob_refcnt。
static int visit_decref(PyObject *op, void *parent) { _PyObject_ASSERT(_PyObject_CAST(parent), !_PyObject_IsFreed(op)); if (_PyObject_IS_GC(op)) { PyGC_Head *gc = AS_GC(op); /* We're only interested in gc_refs for objects in the * generation being collected, which can be recognized * because only they have positive gc_refs. */ if (gc_is_collecting(gc)) { gc_decref(gc); } } return 0; } static void subtract_refs(PyGC_Head *containers) { traverseproc traverse; PyGC_Head *gc = GC_NEXT(containers); for (; gc != containers; gc = GC_NEXT(gc)) { PyObject *op = FROM_GC(gc); traverse = Py_TYPE(op)->tp_traverse; (void) traverse(FROM_GC(gc), (visitproc)visit_decref, op); } }
更新计数值之后,就开始收集不可达对象,将对象移入到不可达列表中。unreachable。
/* A traversal callback for move_unreachable. */ static int visit_reachable(PyObject *op, PyGC_Head *reachable) { if (!_PyObject_IS_GC(op)) { return 0; } PyGC_Head *gc = AS_GC(op); const Py_ssize_t gc_refs = gc_get_refs(gc); if (! gc_is_collecting(gc)) { return 0; } assert(gc->_gc_next != 0); if (gc->_gc_next & NEXT_MASK_UNREACHABLE) { PyGC_Head *prev = GC_PREV(gc); PyGC_Head *next = (PyGC_Head*)(gc->_gc_next & ~NEXT_MASK_UNREACHABLE); _PyObject_ASSERT(FROM_GC(prev), prev->_gc_next & NEXT_MASK_UNREACHABLE); _PyObject_ASSERT(FROM_GC(next), next->_gc_next & NEXT_MASK_UNREACHABLE); prev->_gc_next = gc->_gc_next; // copy NEXT_MASK_UNREACHABLE _PyGCHead_SET_PREV(next, prev); gc_list_append(gc, reachable); gc_set_refs(gc, 1); } else if (gc_refs == 0) { gc_set_refs(gc, 1); } else { _PyObject_ASSERT_WITH_MSG(op, gc_refs > 0, "refcount is too small"); } return 0; } static void move_unreachable(PyGC_Head *young, PyGC_Head *unreachable) { PyGC_Head *prev = young; PyGC_Head *gc = GC_NEXT(young); while (gc != young) { if (gc_get_refs(gc)) { PyObject *op = FROM_GC(gc); traverseproc traverse = Py_TYPE(op)->tp_traverse; _PyObject_ASSERT_WITH_MSG(op, gc_get_refs(gc) > 0, "refcount is too small"); (void) traverse(op, (visitproc)visit_reachable, (void *)young); _PyGCHead_SET_PREV(gc, prev); gc_clear_collecting(gc); prev = gc; } else { prev->_gc_next = gc->_gc_next; PyGC_Head *last = GC_PREV(unreachable); last->_gc_next = (NEXT_MASK_UNREACHABLE | (uintptr_t)gc); _PyGCHead_SET_PREV(gc, last); gc->_gc_next = (NEXT_MASK_UNREACHABLE | (uintptr_t)unreachable); unreachable->_gc_prev = (uintptr_t)gc; } gc = (PyGC_Head*)prev->_gc_next; } // young->_gc_prev must be last element remained in the list. young->_gc_prev = (uintptr_t)prev; // don't let the pollution of the list head's next pointer leak unreachable->_gc_next &= ~NEXT_MASK_UNREACHABLE; }
这段代码的逻辑是,遍历收集代中的所有对象,判断对象的计数值是否为0
如果等于0,则从收集代中移除,加入不可达列表中,然后打上不可达标记。
如果不等于0,则遍历对象的所有元素,如果元素已经被打上不可达标记,则把该元素从不可达列表中移除,重新加入收集代列表中,并且将计数值设置为1。这是因为父对象可以被访问,那么子对象一定可以被访问。
- 把定义了__del__的对象从不可达对象中移除
static int has_legacy_finalizer(PyObject *op) { return Py_TYPE(op)->tp_del != NULL; } static void move_legacy_finalizers(PyGC_Head *unreachable, PyGC_Head *finalizers) { PyGC_Head *gc, *next; assert((unreachable->_gc_next & NEXT_MASK_UNREACHABLE) == 0); for (gc = GC_NEXT(unreachable); gc != unreachable; gc = next) { PyObject *op = FROM_GC(gc); _PyObject_ASSERT(op, gc->_gc_next & NEXT_MASK_UNREACHABLE); gc->_gc_next &= ~NEXT_MASK_UNREACHABLE; next = (PyGC_Head*)gc->_gc_next; if (has_legacy_finalizer(op)) { gc_clear_collecting(gc); gc_list_move(gc, finalizers); } } }
这里的逻辑就比较简单,判断是否定义了__del__函数,如果有,则从不可达列表中删除,加入finalizers列表,并且清除收集中标记。
/* A traversal callback for move_legacy_finalizer_reachable. */ static int visit_move(PyObject *op, PyGC_Head *tolist) { if (_PyObject_IS_GC(op)) { PyGC_Head *gc = AS_GC(op); if (gc_is_collecting(gc)) { gc_list_move(gc, tolist); gc_clear_collecting(gc); } } return 0; } /* Move objects that are reachable from finalizers, from the unreachable set * into finalizers set. */ static void move_legacy_finalizer_reachable(PyGC_Head *finalizers) { traverseproc traverse; PyGC_Head *gc = GC_NEXT(finalizers); for (; gc != finalizers; gc = GC_NEXT(gc)) { /* Note that the finalizers list may grow during this. */ traverse = Py_TYPE(FROM_GC(gc))->tp_traverse; (void) traverse(FROM_GC(gc), (visitproc)visit_move, (void *)finalizers); } }
然后再遍历finalizers列表中的所有对象,判断对象的每个元素是否也是可GC对象,并且也有收集中标记,如果满足条件,则从不可达列表中删除,加入finalizers列表,并且清除收集中标记。
- 遍历不可达对象列表,处理弱引用。
- 遍历不可达对象列表,为每个对象调用tp_finalize函数,如果没有则跳过。
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); }
- 处理复活的对象
static inline void handle_resurrected_objects(PyGC_Head *unreachable, PyGC_Head* still_unreachable, PyGC_Head *old_generation) { // Remove the PREV_MASK_COLLECTING from unreachable // to prepare it for a new call to 'deduce_unreachable' gc_list_clear_collecting(unreachable); // After the call to deduce_unreachable, the 'still_unreachable' set will // have the PREV_MARK_COLLECTING set, but the objects are going to be // removed so we can skip the expense of clearing the flag. PyGC_Head* resurrected = unreachable; deduce_unreachable(resurrected, still_unreachable); clear_unreachable_mask(still_unreachable); // Move the resurrected objects to the old generation for future collection. gc_list_merge(resurrected, old_generation); }
这里主要是上一步会调用tp_finalize
函数,有可能会把一些对象复活,所以需要重新收集一次不可达对象,然后将复活的对象移入老年代中。
- 删除不可达对象
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) { /* 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函数,再看看listobject的tp_dealloc实现。
static void list_dealloc(PyListObject *op) { Py_ssize_t i; PyObject_GC_UnTrack(op); Py_TRASHCAN_BEGIN(op, list_dealloc) if (op->ob_item != NULL) { i = Py_SIZE(op); while (--i >= 0) { Py_XDECREF(op->ob_item[i]); } PyMem_FREE(op->ob_item); } if (numfree < PyList_MAXFREELIST && PyList_CheckExact(op)) free_list[numfree++] = op; else Py_TYPE(op)->tp_free((PyObject *)op); Py_TRASHCAN_END }
首先会调用PyObject_GC_UnTrack,就是将该对象从GC链表中摘除。然后再遍历子元素,将子元素的计数减1。计数减为0时,又会调用对象的tp_dealloc函数。
此番调用下来,lst1和lst2的计数都会被减为0,都会从GC链表中摘除,并且都能释放。解除了环引用。
到此这篇关于一文详解Python垃圾回收的文章就介绍到这了,更多相关Python垃圾回收内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
python sort、sorted高级排序技巧分享(key的使用)
这篇文章主要介绍了python sort、sorted高级排序技巧(key的使用),具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2023-03-03
最新评论