大师兄的Python源码学习笔记(五十五): Python的内存管理机制(十)

大师兄的Python源码学习笔记(五十四): Python的内存管理机制(九)
大师兄的Python源码学习笔记(五十六): Python的内存管理机制(十一)

五、Python中的垃圾收集

3. 标记——清除方法
  • 由于Python采用了三代的分代收集机制,如果当前收集的是第一代,那么在开始垃圾收集之前,Python会通过gc_list_merge将比其年轻的所有代的内存链表链接到第一代内存链表之后。
Modules/gcmodule.c

/*** list functions ***/

static void
gc_list_init(PyGC_Head *list)
{
    list->gc.gc_prev = list;
    list->gc.gc_next = list;
}
Modules/gcmodule.c

/* append list `from` onto list `to`; `from` becomes an empty list */
static void
gc_list_merge(PyGC_Head *from, PyGC_Head *to)
{
    PyGC_Head *tail;
    assert(from != to);
    if (!gc_list_is_empty(from)) {
        tail = to->gc.gc_prev;
        tail->gc.gc_next = from->gc.gc_next;
        tail->gc.gc_next->gc.gc_prev = tail;
        to->gc.gc_prev = from->gc.gc_prev;
        to->gc.gc_prev->gc.gc_next = to;
    }
    gc_list_init(from);
}
  • 下图展示了merge的结果:


  • 此后的标记——清除算法就将在merge之后所得到的那一条内存链表上进行。
  • 在开始之前,我们要先建立一个循环引用的例子,用于解释Python是如何通过标记——清除垃圾收集方法打破循环引用
>>> list1 = []
>>> list2 = []
>>> list1.append(list2)
>>> list2.append(list1)
>>> a = list1
>>> list3 = []
>>> list4 = []
>>> list3.append(list4)
>>> list4.append(list3)

  • 其中PyObject_HEAD部分的数值为对象的引用计数ob_refcnt的值。
2.1 寻找Root Object集合
  • 为了使用标记——清除算法,首先需要找出root object集合,那么哪些container对象应该属于root object哪?
  • 循环引用案例中,两个对象的引用计数都为1,但仅存在它们之间,所以这两个对象都需要被回收。
  • 也就是说,虽然他们的引用计数为1,但有效引用计数为0。
  • 为了从引用计数获得有效引用计数,必须将循环引用的影响去除,具体的实现就是将两个对象各自的引用计数减1:
  • 假设两个对象为A、B;
  • 从A出发,因为它有一个对B的引用,则将B的引用计数减1;
  • 然后顺着引用到B,因为它有一个对A的引用,同样将A的引用计数减1;
  • 通过这种方式完成了循环引用对象间环的摘除。
  • 但这样可能会引出一个问题:
  • 假设可收集对象链表中的container对象A有一个对对象C的引用,而C并不在这个链表中;
  • 如果将C的引用计数减1,而最后A并没有被回收,则C的被错误的减少了1,这将导致在未来的某个时刻出现一个对C的悬空引用**;
  • 为此,更好的做法是并不改动真是的引用计数,而改动引用计数的副本。
  • 而这个副本的唯一作用就是寻找root object集合,它就是PYGC_HEAD中的gc.gc_ref
  • 在垃圾收集的第一步,就是遍历可收集对象链表,并将每个对象的gc.gc_ref值设置为其ob_refcnt值。
Modules/gcmodule.c

/* Set all gc_refs = ob_refcnt.  After this, gc_refs is > 0 for all objects
 * in containers, and is GC_REACHABLE for all tracked gc objects not in
 * containers.
 */
static void
update_refs(PyGC_Head *containers)
{
    PyGC_Head *gc = containers->gc.gc_next;
    for (; gc != containers; gc = gc->gc.gc_next) {
        assert(_PyGCHead_REFS(gc) == GC_REACHABLE);
        _PyGCHead_SET_REFS(gc, Py_REFCNT(FROM_GC(gc)));
        /* Python's cyclic gc should never see an incoming refcount
         * of 0:  if something decref'ed to 0, it should have been
         * deallocated immediately at that time.
         * Possible cause (if the assert triggers):  a tp_dealloc
         * routine left a gc-aware object tracked during its teardown
         * phase, and did something-- or allowed something to happen --
         * that called back into Python.  gc can trigger then, and may
         * see the still-tracked dying object.  Before this assert
         * was added, such mistakes went on to allow gc to try to
         * delete the object again.  In a debug build, that caused
         * a mysterious segfault, when _Py_ForgetReference tried
         * to remove the object from the doubly-linked list of all
         * objects a second time.  In a release build, an actual
         * double deallocation occurred, which leads to corruption
         * of the allocator's internal bookkeeping pointers.  That's
         * so serious that maybe this should be a release-build
         * check instead of an assert?
         */
        assert(_PyGCHead_REFS(gc) != 0);
    }
}
  • 接着将环引用从引用中删除:
Modules/gcmodule.c

/* Subtract internal references from gc_refs.  After this, gc_refs is >= 0
 * for all objects in containers, and is GC_REACHABLE for all tracked gc
 * objects not in containers.  The ones with gc_refs > 0 are directly
 * reachable from outside containers, and so can't be collected.
 */
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);
    }
}
  • 其中的traverse是与特定的container对象相关的,在container对象的类型对象中定义。
  • 一般来说,traverse的工作是遍历container对象中的每一个引用,然后对引用进行某种动作。
  • 这个动作在subtract_refs中就是visit_decref,它以一个回调函数的形式传递到traverse操作中。
  • 在完成了subtract_refs后,可收集对象链表中所有container对象之间的环引用都被摘除了。
  • 这时,有一些container对象的PYGC_Head.gc.gc_ref还不为0,这意味着存在对这些对象的外部引用。
  • 而这些对象,就是开始标记——清除算法root object集合。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容