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;
P服务器托管网yObject **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
比如调用gc.collect(2)
,就表示启动全部的垃圾回收。这里就会将第0、1代的对象合并到第2代上。合并之后第0、1代上就空了,全部可GC的对象都在第2代上。
- 推断不可达对象
/* handy references */
young = GEN_HEAD(gcstate, generation);
if (generation
这里的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)
这里的逻辑就是遍历所有对象,然后赋值_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) 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) {
/* o服务器托管网bject 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)
所以说,定义了__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 tp_free((PyObject *)op);
Py_TRASHCAN_END
}
首先会调用PyObject_GC_UnTrack,就是将该对象从GC链表中摘除。然后再遍历子元素,将子元素的计数减1。计数减为0时,又会调用对象的tp_dealloc函数。
此番调用下来,lst1和lst2的计数都会被减为0,都会从GC链表中摘除,并且都能释放。解除了环引用。
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.fwqtg.net
相关推荐: Nashorn引擎导致metaspace oom
从报错内容很清楚是Metaspace区域oom了 大部分情况下,程序运行中不会出现过多的类加载数量的变动,先导入dump文件检查是否有异常的classLoader或者有异常动态生成的class 发现了下面这个classLoader数量异常,项目中用…