[Python]垃圾回收机制

Python 垃圾回收机制


python采用计数引用(Reference Counting)为主,来跟踪和回收垃圾。采用标记清除(Mark and Sweep)来解决容器可能产生的循环引用问题、分代回收(Generation Collection)以空间换时间的方法提高垃圾回收效率。

引用计数

Python中一切皆对象,它们的核心就是一个结构体PyObject,其中有个obj_refcnt字段,用于记录对象被引用的次数:当一个对象有新的引用时,它的obj_refcount 加1;当对象的引用被删除时,该对象的obj_refcount减1;当一个对象的obj_refcount = 0 时,则说明它不再被其他对象引用,就可以回收了。

//object.h
typedef struct_object{
    int obj_refcnt;
    struct_typeobject *obtype;
} PyObject;

例如下面是int类型的定义

//intobject.h
typedef struct{
    PyObject_HEAD
    long obj_ival;
} PyIntObject;

优点

  1. 1.高效

  1. 2.实时性:运行期没有停顿。一旦没有引用就直接释放掉了,不用像其他机制等到特定的时机。将处理回收的时间平摊到了平时。
    1. 3.对象有确定的生命周期。
      1. 4.易于实现

缺点

  1. 1.维护引用计数消耗资源

  1. 2. 循环引用
    循环引用导致 obj_refcount 一直不为零,造成 内存泄漏(即内存一直不能释放,造成资源浪费)。所以需要标记-清除机制来解决循环引用问题。

标记清除

循环引用问题

可以包含其他对象的容器对象(list,set,dict,class)都可能产生循环引用问题,因此在申请内存时,所有容器对象头部都加了_GC_Head 来实现“标记-清除“机制。

怎样做?

实现思路

为解决对象之间循环引用问题,借鉴Ruby的标记清除机制,Python轮询0代对象,将循环引用的对象的引用计数减1。

// objimpl.h
typedef union _gc_head {
    struct {
        union _gc_head *gc_next;
        union _gc_head *gc_prev;
        Py_ssize_t gc_refs;
    } gc;
    long double dummy;  /* force worst-case alignment */
} PyGC_Head;

为对象申请内存的时候,可以明显的看到,实际申请内存的数量已经加上了_GC_Head 的大小。

// gcmodule.c
PyObject *
_PyObject_GC_Malloc(size_t basicsize)
{
    PyObject *op;
    PyGC_Head *g = (PyGC_Head *)PyObject_MALLOC(
                sizeof(PyGC_Head) + basicsize);
    if (g == NULL)
        return PyErr_NoMemory();

    ......

    op = FROM_GC(g);
    return op;
}

举个栗子,从list 对象的创建中,主要有以下逻辑:

// listobject.c
PyObject *
PyList_New(Py_ssize_t size)
{
    PyListObject *op;
    ......
    op = PyObject_GC_New(PyListObject, &PyList_Type);
    ......
    _PyObject_GC_TRACK(op);
    return (PyObject *) op;
}

PyObject_TRACK 就将对象链接到了第0代对象集合中。
垃圾回收时,先将对象引用计数复制一份副本(以免在操作过程中破坏真实的引用计数值):

// gcmodule.c
static void
update_refs(PyGC_Head *containers)
{
    PyGC_Head *gc = containers->gc.gc_next;
    for (; gc != containers; gc = gc->gc.gc_next) {
        assert(gc->gc.gc_refs == GC_REACHABLE);
        gc->gc.gc_refs = FROM_GC(gc)->ob_refcnt;
        assert(gc->gc.gc_refs != 0);
    }
}

然后操作这个副本,遍历对象集合,将被引用对象的引用副本减1。

// gcmodule.c
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 = FROM_GC(gc)->ob_type->tp_traverse;
        (void) traverse(FROM_GC(gc),
                   (visitproc)visit_decref,
                   NULL);
    }
}

这个traverse是对象类型定义的函数,用来遍历对象,通过传入的回调函数visit_decref来操作引用计数副本。
例如dict就要在key和value上都用visit_decref操作一遍:

// dictobject.c
static int
dict_traverse(PyObject *op, visitproc visit, void *arg)
{
    Py_ssize_t i = 0;
    PyObject *pk;
    PyObject *pv;

    while (PyDict_Next(op, &i, &pk, &pv)) {
        visit(pk);
        visit(pv);
    }
    return 0;
}

然后根据引用计数副本值是否为0将集合内的对象分成两类,reachableunreachable,其中unreachable是可以被回收的对象:

// gcmodule.c
static void
move_unreachable(PyGC_Head *young, PyGC_Head *unreachable)
{
    PyGC_Head *gc = young->gc.gc_next;
    while (gc != young) {
        PyGC_Head *next;
        if (gc->gc.gc_refs) {
            PyObject *op = FROM_GC(gc);
            traverseproc traverse = op->ob_type->tp_traverse;
            assert(gc->gc.gc_refs > 0);
            gc->gc.gc_refs = GC_REACHABLE;
            (void) traverse(op,
                            (visitproc)visit_reachable,
                            (void *)young);
            next = gc->gc.gc_next;
        }
        else {
            next = gc->gc.gc_next;
            gc_list_move(gc, unreachable);
            gc->gc.gc_refs = GC_TENTATIVELY_UNREACHABLE;
        }
        gc = next;
    }
}

分代回收

是什么?

将系统中所有的内存块根据其存活时间划分为不同的集合,每个集合就成为一“代“,垃圾收集频率随着“代“的存活时间增大而减少,存活时间通常利用经过几次垃圾回收来度量。

也就是说新对象被定义为0代,每次创建新对象时,python会检测0代,满了就开始回收,从0代出发,检测循环引用,释放引用数为0的对象。

为什么?

为什么是从新对象开始清理呢?

年轻的对象死的快,而越是年老的对象可能存活更长的时间。
——弱代假说

怎样做?

用来表示“代“的结构体是gc_generation,包括了当前代链表表头、对象数量上限、当前对象数量:

// gcmodule.c
struct gc_generation {
    PyGC_Head head;
    int threshold; /* collection threshold */
    int count; /* count of allocations or collections of younger
              generations */
};

Python默认定义了三代对象集合,索引数越大,对象存活时间越长。

#define NUM_GENERATIONS 3
#define GEN_HEAD(n) (&generations[n].head)

/* linked lists of container objects */
static struct gc_generation generations[NUM_GENERATIONS] = {
    /* PyGC_Head,               threshold,  count */
    {{{GEN_HEAD(0), GEN_HEAD(0), 0}},   700,        0},
    {{{GEN_HEAD(1), GEN_HEAD(1), 0}},   10,     0},
    {{{GEN_HEAD(2), GEN_HEAD(2), 0}},   10,     0},
};

新生成的对象会被加入第0代,前面_PyObject_GC_Malloc中省略的部分就是Python GC触发的时机。每新生成一个对象都会检查第0代有没有满,如果满了就开始着手进行垃圾回收

 g->gc.gc_refs = GC_UNTRACKED;
 generations[0].count++; /* number of allocated GC objects */
 if (generations[0].count > generations[0].threshold &&
     enabled &&
     generations[0].threshold &&
     !collecting &&
     !PyErr_Occurred()) {
          collecting = 1;
          collect_generations();
          collecting = 0;
 }

[1]: 简书: http://www.jianshu.com/p/1e375fb40506

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值