深入理解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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章