设为首页 加入收藏

TOP

Python垃圾回收(六)
2023-09-09 10:25:29 】 浏览:209
Tags:Python
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),也就是该对象还没有被移除,这种情况则清除该对象的收集中标记,然后移入老年代中。

  1. 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的垃圾回收主要用到了

  1. 引用计数
  2. 标记清除
  3. 分代回收

其中分代回收步骤为

  1. 将年轻代的对象移动到指定回收代的列表后。
  2. 遍历回收代列表,将对象设置为收集中PREV_MASK_COLLECTING标记,然后将引用计数复制一份到_gc_prev中
  3. 然后遍历每个对象中的每个元素,如果这个元素也是可GC对象,并且也有收集中标记,则将_gc_prev中的计数值减1
  4. 再遍历回收代列表,判断_gc_prev计数值是否为0,
    1. 如果为0,则标记为不可达,然后移动到不可达列表中。
    2. 如果不为0,则遍历该对象的元素,如果该元素已经标记为清除,就把该元素移动到原回收代列表中。(也就是父对象仍然可达,则子对象也可达)。然后清除该对象的收集中标记。
  5. 遍历不可达列表,清除不可达标记,判断是否定义了__del__函数,如果有,则将清除收集中标记,并移入finalizers列表中。
  6. 遍历finalizers列表的每个对象,判断对象中的元素是否是可GC对象,并且有收集中标记,将该元素清除收集中标记,移入finalizers列表中。
  7. 遍历不可达列表, 处理弱引用
  8. 遍历不可达列表的每个对象,调用对象的tp_finalize函数,如果没有则跳过。
  9. 遍历不可达列表,将复活对象移到老年代列表中,其他对象移动到仍然不可达列表final_unreachable
  10. 最后遍历 final_unreachable 列表,为每个对象调用tp_clear函数
    1. 如果真的可以删除,则把自己从对应GC列表中摘除
    2. 如果还不能删除,则清除对象的收集中标记,对象重新加入老年代中。
  11. 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了。出现环引用,无法释放。
这个时候就轮到标记清楚和分代回收解决了。

  1. 首先会将第0、1代的元素移到第2代上。因为gc.collect(2)
  2. 然后遍历第2代列表,为每个对象设置收集中标记,将对象的真实计数复制到_gc_prev中。
  3. 再遍历第2代列表,判断对象的子元素是否也是 可GC对象、也有收集中标记,如果有则将该元素计数值减1。
    1. 此时 lst1、lst2的_gc_prev计数值都为0
  4. 然后将_gc_prev计数值为0的对象移入不可达列表中。
  5. 因为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

首页 上一页 3 4 5 6 下一页 尾页 6/6/6
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇【python技巧】替换文件中的某几行 下一篇Python名称空间和作用域,闭包函数

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目