深入理解Python GC

对象内存管理

python中对于对象内存管理有两种方法,引用计数/GC 。</br>
引用计数策略应用到每个对象的管理中,接收/返回对象都需要+-对象的计数,而对象是否支持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对象内存分配过程:</br>

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遍历对象。</br>

确定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;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容