乐趣区

深入理解Python-GC

对象内存管理

python 中对于对象内存管理有两种方法,引用计数 /GC。
引用计数策略应用到每个对象的管理中,接收 / 返回对象都需要 +- 对象的计数,而对象是否支持 GC 则是可选的,因为 GC 的存在是为了解决引用计数留下的循环引用问题,对于没有包含其他对象指针的对象可以不支持 GC。

引用计数

引用计数的优势在于简单,把对象销毁时间分摊到程序生命周期

static PyObject* PyPerson_New(PyTypeObject *type, PyObject *args, PyObject *kwds){PyObject *ret = create_person(...);

    // 发生异常 销毁对象
    if(PyErr_Occurred()){Py_XDECREF(ret);
        return NULL;
    }


    if(ret == NULL)
        FAST_RETURN_ERROR("create person obj fail");
    
    if(!check_person(ret)){Py_XDECREF(ret); // 销毁对象 -1
         Py_XINCREF(Py_None); // 返回对象 +1
        return Py_None;
    }
    
    return ret;
}

上面的实例代码演示了返回对象计数 +1 和 对象超出作用于计数 -1
当对象计数 == 0 的时候 调用 typeobject 的 tp_dealloc 函数完成对象清理

#define Py_DECREF(op)                                   \
    do {                                                \
        PyObject *_py_decref_tmp = (PyObject *)(op);    \
        if (_Py_DEC_REFTOTAL  _Py_REF_DEBUG_COMMA       \
        --(_py_decref_tmp)->ob_refcnt != 0)             \
            _Py_CHECK_REFCNT(_py_decref_tmp)            \
        else                                            \
            _Py_Dealloc(_py_decref_tmp);                \
    } while (0)

当然引用计数也有众所周知的缺点, 循环引用, 所以还是需要引入 GC 机制来弥补

GC

python gc 使用的策略是标记 - 清除,根据是否从 root object 可达判断一个对象是否是垃圾对象

对象支持 GC

由于是否支持 GC 是可选的,所以我们要主动选择对象是否支持 GC,只需要在 typeobject 中加入一个标记就好 Py_TPFLAGS_HAVE_GC

GC 对象内存模型

gc 对象内除了对象本身的数据,还增加了一些 gc 信息,具体可以看看 gc 对象内存分配过程:

PyObject *
PyType_GenericAlloc(PyTypeObject *type, Py_ssize_t nitems)
{
//......
    if (PyType_IS_GC(type))
        obj = _PyObject_GC_Malloc(size);
    else
        obj = (PyObject *)PyObject_MALLOC(size);
//.....
}

static PyObject *
_PyObject_GC_Alloc(int use_calloc, size_t basicsize)
{
    PyObject *op;
    PyGC_Head *g;
    size_t size;
    if (basicsize > PY_SSIZE_T_MAX - sizeof(PyGC_Head))
        return PyErr_NoMemory();
    size = sizeof(PyGC_Head) + basicsize;
//....
}

可以看出 gc 对象内存模型如下:

————-----
|gc head|
|-------|
|  obj  |
|       |
————-----
// 通过 PyGC_Head 把 gc 对象形成链表
typedef union _gc_head {
    struct {
        union _gc_head *gc_next;
        union _gc_head *gc_prev;
        Py_ssize_t gc_refs;  //gc 对象的状态
    } gc;
    double dummy;  /* force worst-case alignment */
} PyGC_Head;
/ 设置为 untrack 未追踪
g->gc.gc_refs = 0;
_PyGCHead_SET_REFS(g, GC_UNTRACKED);  
// 把新对象统计在第 0 代
_PyRuntime.gc.generations[0].count++; /* number of allocated GC objects */
// 如果第 0 代对象数超过了阈值 触发 gc
if (_PyRuntime.gc.generations[0].count > _PyRuntime.gc.generations[0].threshold &&
    _PyRuntime.gc.enabled &&
    _PyRuntime.gc.generations[0].threshold &&
    !_PyRuntime.gc.collecting &&
    !PyErr_Occurred()) {
    _PyRuntime.gc.collecting = 1;
    collect_generations(); //gc
    _PyRuntime.gc.collecting = 0;
}

python gc 也是分代,同样具有新生代、老年代的表现形式,和 jvm gcheap 分代不同的是 这里的代只是统计意义不具备内存占用

注意到走完对象内存分配的流程,对象其实还没有真正的分配到某一代中
在 PyObject_GC_Alloc 中分配完内存之后才会执行这一步

 if (PyType_IS_GC(type))
        _PyObject_GC_TRACK(obj);  // 加入到对象链表


把对象加入到第 0 代的对象链表
#define _PyObject_GC_TRACK(o) do { \
    PyGC_Head *g = _Py_AS_GC(o); \
    if (_PyGCHead_REFS(g) != _PyGC_REFS_UNTRACKED) \
        Py_FatalError("GC object already tracked"); \
    _PyGCHead_SET_REFS(g, _PyGC_REFS_REACHABLE); \
    g->gc.gc_next = _PyGC_generation0; \
    g->gc.gc_prev = _PyGC_generation0->gc.gc_prev; \
    g->gc.gc_prev->gc.gc_next = g; \
    _PyGC_generation0->gc.gc_prev = g; \
    } while (0);

GC 过程

要筛选出垃圾对象,最直接的方法就是从 rootobject 开始把所有能访问到的对象都标记上,剩下的就是垃圾对象了。这个过程需要做两个事,1. 确定哪些是 rootobject 2. 从 rootobject 遍历对象。

确定 GC 范围


    static Py_ssize_t
    collect_generations(void)
    {
        int i;
        Py_ssize_t n = 0;
    // 查找最老的 超出阈值的代
        for (i = NUM_GENERATIONS-1; i >= 0; i--) {if (_PyRuntime.gc.generations[i].count > _PyRuntime.gc.generations[i].threshold) {
        
    // 如果 long_lived 对象不是很多 则避免 full gc
                if (i == NUM_GENERATIONS - 1
                    && _PyRuntime.gc.long_lived_pending < _PyRuntime.gc.long_lived_total / 4)
                    continue;
                n = collect_with_callback(i); // 收集 gen[i] - gen[0]
                break;
            }
        }
        return n;
    }

对象遍历

在确定 rootobject 之前,我们要先解决对象遍历的问题。因为我们需要从一个对象开始访问它引用的对象, 也就是广度优先遍历,所以不能直接遍历 gc 对象链表,而是使用额外的机制。

static void
subtract_refs(PyGC_Head *containers)
{
    traverseproc traverse;
    PyGC_Head *gc = containers->gc.gc_next;
    for (; gc != containers; gc=gc->gc.gc_next) {traverse = Py_TYPE(FROM_GC(gc))->tp_traverse;
        (void) traverse(FROM_GC(gc),
                       (visitproc)visit_decref,
                       NULL);
    }
}

这个遍历机制就是 typeobject 中的 tp_traverse 函数,在 tp_traverse 函数中对象必须把引用到的对象交给函数 visitproc 处理,这样就完成了对象的广度优先遍历。

static int person_traverse(PyObject *self, visitproc visit, void *arg){Person *p = (Person*)self;
    //visit(p->dict,arg)
    Py_VISIT(p->dict);

    return 0;
}

确定 rootobject

所有对象和对象直接的引用形成了一个有向图,先把对象之间的引用去掉,那么最后计数 >0 的表明对象存在非对象间引用 也就是 rootobject

接回上面的例子,visit_decref 就是用来把对象中的引用 - 1 的函数

static int
visit_decref(PyObject *op, void *data)
{if (PyObject_IS_GC(op)) {PyGC_Head *gc = AS_GC(op);
  
        if (_PyGCHead_REFS(gc) > 0)
            _PyGCHead_DECREF(gc);
    }
    return 0;
}

完成第一轮筛选后,把计数 >0 的标记未 reachable,计数 == 0 的标记为 unreachable


static void
move_unreachable(PyGC_Head *young, PyGC_Head *unreachable)
{
    PyGC_Head *gc = young->gc.gc_next;

        while (gc != young) {
        PyGC_Head *next;
//refs > 0 经过上面的 refs-1  root object refs>0
        if (_PyGCHead_REFS(gc)) {PyObject *op = FROM_GC(gc);
            traverseproc traverse = Py_TYPE(op)->tp_traverse;
            assert(_PyGCHead_REFS(gc) > 0);
// 设置对象为 reachable
            _PyGCHead_SET_REFS(gc, GC_REACHABLE);
// 从这个 rootobject  能访问到的对象都是 reachable
            (void) traverse(op,
                            (visitproc)visit_reachable,
                            (void *)young);
            next = gc->gc.gc_next;
            if (PyTuple_CheckExact(op)) {_PyTuple_MaybeUntrack(op);
            }
        }
        else {
//unreachable  这里会误判 遍历的时候会修正
            next = gc->gc.gc_next;
            gc_list_move(gc, unreachable);
            _PyGCHead_SET_REFS(gc, GC_TENTATIVELY_UNREACHABLE);
        }
        gc = next;
    }
}



static int
visit_reachable(PyObject *op, PyGC_Head *reachable)
{if (PyObject_IS_GC(op)) {PyGC_Head *gc = AS_GC(op);
        const Py_ssize_t gc_refs = _PyGCHead_REFS(gc);

        if (gc_refs == 0) {
// 计数 +1  这样等下遍历到它就会归为 reachable
            _PyGCHead_SET_REFS(gc, 1);
        }
        else if (gc_refs == GC_TENTATIVELY_UNREACHABLE) {
      
// 上面遍历的时候误判了 把对象放回 reachable 链表
            gc_list_move(gc, reachable);
            _PyGCHead_SET_REFS(gc, 1);
        }
     
        }
    return 0;
}

存活对象迁移

完成了 reachable 对象和 unreachable 对象筛选后,存活对象需要移动到老年代中

if (young != old) {// 如果是 gen[1] 存活数到统计起来 这个会影响到 full gc
    if (generation == NUM_GENERATIONS - 2) {_PyRuntime.gc.long_lived_pending += gc_list_size(young);
    }
// 存活对象进入到更老的 gen 
    gc_list_merge(young, old);
}
else {
// 如果是 full gc 会 untrack 掉 dict 对象减轻 gc 负担
    untrack_dicts(young);
    _PyRuntime.gc.long_lived_pending = 0;
// 对象进入 long lived 状态 
    _PyRuntime.gc.long_lived_total = gc_list_size(young);
}

距离真正完成对象筛选还是差最后一步,因为设计遗留问题如果对象实现了 tp_del 函数 会有一些麻烦。因为有的对象会在 tp_dealloc、tp_free 中调用引用对象的 tp_del 做清理,但是 gc 并不能保证 A 引用 B,B 一定比 A 销毁晚,如果 B 销毁了,A 还调用 B 的 tp_del 会导致内存错误,所以实现了 tp_del 的对象会被放弃收集。为了让程序员有机会手动去清理这部分对象,gc 会把这部分对象存放到 garbage 链表中。

gc_list_init(&finalizers);
// 实现了 tp_del 的对象移动到 finalizers 链表
move_legacy_finalizers(&unreachable, &finalizers);
// 设置为 reachable
move_legacy_finalizer_reachable(&finalizers);
// 存放到 garbage 链表 让程序员自己处理
handle_legacy_finalizers(&finalizers, old);

对象清理


static void
delete_garbage(PyGC_Head *collectable, PyGC_Head *old)
{
    inquiry clear;

    while (!gc_list_is_empty(collectable)) {
        PyGC_Head *gc = collectable->gc.gc_next;
        PyObject *op = FROM_GC(gc);
// 定义了 DEBUG_SAVEALL 会导致不清除 而是存放到 grabage 链表
        if (_PyRuntime.gc.debug & DEBUG_SAVEALL) {PyList_Append(_PyRuntime.gc.garbage, op);
        }
        else {if ((clear = Py_TYPE(op)->tp_clear) != NULL) {
// 调用 tp_clear
               Py_INCREF(op);
                clear(op); 
                Py_DECREF(op); 
            }
        }

tp_clear 要做的就是引用对象计数 -1, 把对象从 unreachable 移除, 释放对象内存回内存池

static int person_clear(PyObject *self){Person *p = (Person*)self;
    Py_CLEAR(p->dict);
    //PyObject_GC_Del
    Py_TYPE(self)->tp_free(self);
    return 0;
}
退出移动版