python源码复习笔记

Tags:

以前看过python源码,没记笔记,忘光了,现在重新瞧瞧。

各种对象的实现

通用部分

PyObject_HEAD 和 PyObject_VAR_HEAD

/* PyObject_HEAD defines the initial segment of every PyObject. */
#define PyObject_HEAD                   \
    _PyObject_HEAD_EXTRA                \
    Py_ssize_t ob_refcnt;               \
    struct _typeobject *ob_type;

#define PyObject_VAR_HEAD               \
    PyObject_HEAD                       \
    Py_ssize_t ob_size; /* Number of items in variable part */

这2个东西会出现在各种对象的结构定义里。obj_refcnt显然是引用计数,ob_type是类型元信息的指针,ob_size是变长对象的对象数量信息。

PyIntObject 普通整数(long)

文件:

  • intobject.h
  • intobject.c
typedef struct {
    PyObject_HEAD
    long ob_ival;
} PyIntObject;

应该是最简单的对象类型了,用一个long存数据信息。

下面是整数对象的类型元信息,其实就是自定义实现了object.h里的_typeobject:

PyTypeObject PyInt_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "int", // tp_name 用于打印
    sizeof(PyIntObject), // tp_basicsize 
    0, // tp_itemsize 因为不是变长类型,所以为0
    // 下面是各种函数指针
    (destructor)int_dealloc,                    /* tp_dealloc */
    (printfunc)int_print,                       /* tp_print */
    ···  
    &int_as_number,                             /* tp_as_number */
    ···
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_CHECKTYPES |
        Py_TPFLAGS_BASETYPE | Py_TPFLAGS_INT_SUBCLASS,          /* tp_flags */
    ···
};

PyIntObject的代数运算过程中,可能会转换成PyLongObject。

创建

总共4个C API:

PyAPI_FUNC(PyObject *) PyInt_FromString(char*, char**, int);
PyAPI_FUNC(PyObject *) PyInt_FromLong(long);
PyAPI_FUNC(PyObject *) PyInt_FromSize_t(size_t);
PyAPI_FUNC(PyObject *) PyInt_FromSsize_t(Py_ssize_t);

第1、3、4个API会根据传入的值的大小,选择创建PyIntObject还是PyLongObject。

所以主要看PyInt_FromLong。

PyObject *
PyInt_FromLong(long ival)
{
    register PyIntObject *v;
    // small_ints
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
    if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) {
        v = small_ints[ival + NSMALLNEGINTS];
        Py_INCREF(v);
        ···
        return (PyObject *) v;
    }
#endif
    if (free_list == NULL) {
        if ((free_list = fill_free_list()) == NULL)
            return NULL;
    }
    /* Inline PyObject_New */
    v = free_list;
    free_list = (PyIntObject *)Py_TYPE(v);
    (void)PyObject_INIT(v, &PyInt_Type);
    v->ob_ival = ival;
    return (PyObject *) v;
}

这段代码涉及到了2大机制,一个是small_ints:

#define NSMALLPOSINTS           257
#define NSMALLNEGINTS           5
static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];

python最多会缓存262个小整数,PyInt_FromLong每次检测val是不是在小整数范围,是的话就复用small_ints里的对象。

small_ints是在python启动的时候就先初始化的:

int
_PyInt_Init(void)
{
    PyIntObject *v;
    int ival;
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
    for (ival = -NSMALLNEGINTS; ival < NSMALLPOSINTS; ival++) {
        if (!free_list && (free_list = fill_free_list()) == NULL)
            return 0;
        /* PyObject_New is inlined */
        v = free_list;
        free_list = (PyIntObject *)Py_TYPE(v);
        (void)PyObject_INIT(v, &PyInt_Type);
        v->ob_ival = ival;
        small_ints[ival + NSMALLNEGINTS] = v;
    }
#endif
    return 1;
}

这里面已经用到了free_list。

另一个机制是内存池,一块内存放几百个整数,并且会复用内存,改善了性能:

#define BLOCK_SIZE      1000    /* 1K less typical malloc overhead */
#define BHEAD_SIZE      8       /* Enough for a 64-bit pointer */
#define N_INTOBJECTS    ((BLOCK_SIZE - BHEAD_SIZE) / sizeof(PyIntObject))

// 连续存放多个PyIntObject的内存块
// 块与块之间用next指针串起来
struct _intblock {
    struct _intblock *next;
    PyIntObject objects[N_INTOBJECTS];
};


typedef struct _intblock PyIntBlock;

// 当前int内存块的头节点
static PyIntBlock *block_list = NULL;
// 可用的PyIntObject链表的头
static PyIntObject *free_list = NULL;

static PyIntObject *
fill_free_list(void)
{
    PyIntObject *p, *q;
    /* Python's object allocator isn't appropriate for large blocks. */
    p = (PyIntObject *) PyMem_MALLOC(sizeof(PyIntBlock));
    if (p == NULL)
        return (PyIntObject *) PyErr_NoMemory();
    ((PyIntBlock *)p)->next = block_list;
    block_list = (PyIntBlock *)p;
    /* Link the int objects together, from rear to front, then return
       the address of the last int object in the block. */
    // 这里用Py_TYPE指针,把内存块里的所有对象都串成链表,
    // 不过要注意,串的方向是逆的,从后往前,函数最后返回的是最后一个对象
    p = &((PyIntBlock *)p)->objects[0];
    q = p + N_INTOBJECTS;
    while (--q > p)
        Py_TYPE(q) = (struct _typeobject *)(q-1);
    Py_TYPE(q) = NULL;
    return p + N_INTOBJECTS - 1;
}

每次一个PyIntObject被回收,就会放进free_list的头:

static void
int_dealloc(PyIntObject *v)
{
    if (PyInt_CheckExact(v)) {
        Py_TYPE(v) = (struct _typeobject *)free_list;
        free_list = v;
    }
    else
        Py_TYPE(v)->tp_free((PyObject *)v);
}

PyBoolObject

bool其实就是int:

typedef PyIntObject PyBoolObject;

不过True和False不会有多个实例,而是各一个,然而重复使用:

/* Py_False and Py_True are the only two bools in existence.
Don't forget to apply Py_INCREF() when returning either!!! */

/* Don't use these directly */
PyAPI_DATA(PyIntObject) _Py_ZeroStruct, _Py_TrueStruct;

/* Use these macros */
#define Py_False ((PyObject *) &_Py_ZeroStruct)
#define Py_True ((PyObject *) &_Py_TrueStruct)

/* Named Zero for link-level compatibility */
PyIntObject _Py_ZeroStruct = {
    PyObject_HEAD_INIT(&PyBool_Type)
    0
};

PyIntObject _Py_TrueStruct = {
    PyObject_HEAD_INIT(&PyBool_Type)
    1
};

PyLongObject 任意大整数

文件:

  • longobject.h
  • longobject.c
  • longintrepr.h

这个longObject反而不是用long实现了,而是用了复杂的大数运算技巧。

struct _longobject {
    PyObject_VAR_HEAD
    digit ob_digit[1];
};

typedef PY_UINT32_T digit;

typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */

PyLongObject的创建和销毁都是直接在堆内存中发生,没有什么复杂优化:

// _PyLong_New是根据数字digit的数量来创建的,而不是数值上的大小
PyLongObject *
_PyLong_New(Py_ssize_t size)
{
    if (size > (Py_ssize_t)MAX_LONG_DIGITS) {
        PyErr_SetString(PyExc_OverflowError,
                        "too many digits in integer");
        return NULL;
    }
    /* coverity[ampersand_in_size] */
    /* XXX(nnorwitz): PyObject_NEW_VAR / _PyObject_VAR_SIZE need to detect
       overflow */
    return PyObject_NEW_VAR(PyLongObject, &PyLong_Type, size);
}


static void
long_dealloc(PyObject *v)
{
    Py_TYPE(v)->tp_free(v);
}

PyFloatObject 普通浮点数(double)

typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

可见对于double范围内的浮点数,都是用double实现的。

PyObject *
PyFloat_FromDouble(double fval)
{
    register PyFloatObject *op;
    if (free_list == NULL) {
        if ((free_list = fill_free_list()) == NULL)
            return NULL;
    }
    /* Inline PyObject_New */
    op = free_list;
    free_list = (PyFloatObject *)Py_TYPE(op);
    (void)PyObject_INIT(op, &PyFloat_Type);
    op->ob_fval = fval;
    return (PyObject *) op;
}

观察创建函数,发现也有free_list对象池机制。不过就没有small_ints了,毕竟是实数。

PyStringObject

typedef struct {
    PyObject_VAR_HEAD
    long ob_shash;
    int ob_sstate;
    char ob_sval[1];

    /* Invariants:
     *     ob_sval contains space for 'ob_size+1' elements.
     *     ob_sval[ob_size] == 0.
     *     ob_shash is the hash of the string or -1 if not computed yet.
     *     ob_sstate != 0 iff the string object is in stringobject.c's
     *       'interned' dictionary; in this case the two references
     *       from 'interned' to this object are *not counted* in ob_refcnt.
     */
} PyStringObject;

根据注释看,ob_sval存了ob_size+1个字符字节,+1是为了存'\0';ob_shash相当于这个字符串的id,用来快速比对字符串;ob_sstate和缓冲池设计有关。

intern机制

  • 这个机制默认只对长度为0或1的字符串有用。如果相对更长的字符串启用,那么要使用PyString_InternFromString。
  • intern里的字符串是会销毁的,条件是引用计数降到0,因此intern里的PyObject并不持有引用(因为有k、v2个引用,所以是-2)
  • 普通创建的intern字符串是SSTATE_INTERNED_MORTAL的,即会销毁,不过IMMORTAL的intern字符串并没有使用到,所以可认为所有都是mortal的。
  • 实际上intern机制并不会减少字符串内存分配开销,因为intern机制是发生在内存分配之后的。
  • 之所以要创建临时的对象,是因为PyDict_GetItem(PyObject *op, PyObject *key),查找参数是PyObject,必须创建一个临时对象来作为参数去调用PyDict_GetItem。
  • 唯一不会创建临时对象的情况是,空字符串或单字符串,这是通过记录static的characters数组和*nullstring实现的。

连接字符串问题

一句话总结,用+号来连接字符串是低效的,要做N-1次内存分配;正确的做法是用join函数。

PyListObject

typedef struct {
    PyObject_VAR_HEAD
    /* Vector of pointers to list elements.  list[0] is ob_item[0], etc. */
    PyObject **ob_item;

    /* ob_item contains space for 'allocated' elements.  The number
     * currently in use is ob_size.
     * Invariants:
     *     0 <= ob_size <= allocated
     *     len(list) == ob_size
     *     ob_item == NULL implies ob_size == allocated == 0
     * list.sort() temporarily sets allocated to -1 to detect mutations.
     *
     * Items must normally not be NULL, except during construction when
     * the list is not yet visible outside the function that builds it.
     */
    Py_ssize_t allocated;
} PyListObject;

基本特点

  • 也有free_list对象池优化,默认80个
  • list的设计像STL的vector
  • 因为存的是指针,所以移动元素的开销比较小,插入元素,就会发生大量元素挪动
  • list_dealloc会销毁元素列表并回收内存,以及尝试把list对象放进free_list里

list_resize

  • 如果newsize在[allocated, allocated/2]范围,那么不用动内存
  • 如果newsize小于allocated/2,还会发生内存收缩,以节省内存

PyDictObject

基本特点

  • 基于散列表而不是二叉树
  • 冲突解决基于开放定址法,而不是开链

结构


// 表示每个dict至少有8个entry
#define PyDict_MINSIZE 8

typedef struct {
    /* Cached hash code of me_key.  Note that hash codes are C longs.
     * We have to use Py_ssize_t instead because dict_popitem() abuses
     * me_hash to hold a search finger.
     */
    Py_ssize_t me_hash;
    PyObject *me_key;
    PyObject *me_value;
} PyDictEntry;

/*
To ensure the lookup algorithm terminates, there must be at least one Unused
slot (NULL key) in the table.
The value ma_fill is the number of non-NULL keys (sum of Active and Dummy);
ma_used is the number of non-NULL, non-dummy keys (== the number of non-NULL
values == the number of Active items).
To avoid slowing down lookups on a near-full table, we resize the table when
it's two-thirds full.
*/
typedef struct _dictobject PyDictObject;
struct _dictobject {
    PyObject_HEAD
    Py_ssize_t ma_fill;  /* # Active + # Dummy */
    Py_ssize_t ma_used;  /* # Active */
    // slots等于2的幂,而ma_mask = slots - 1,mask除了记录了slots数之外还有别的用途  
    Py_ssize_t ma_mask;
    PyDictEntry *ma_table;
    PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash);
    PyDictEntry ma_smalltable[PyDict_MINSIZE];
};

entry的状态机

  • active: 被使用中
  • dummy:已移除,但因为还在冲突链中,所以不可使用
  • unused:未使用

1.png

PyDict_New

PyObject *
PyDict_New(void)
{
    register PyDictObject *mp;
    if (dummy == NULL) { /* Auto-initialize dummy */
        dummy = PyString_FromString("<dummy key>");
        if (dummy == NULL)
            return NULL;
    }
    if (numfree) {
        mp = free_list[--numfree];
        assert (mp != NULL);
        assert (Py_TYPE(mp) == &PyDict_Type);
        _Py_NewReference((PyObject *)mp);
        if (mp->ma_fill) {
            EMPTY_TO_MINSIZE(mp);
        } else {
            /* At least set ma_table and ma_mask; these are wrong
               if an empty but presized dict is added to freelist */
            INIT_NONZERO_DICT_SLOTS(mp);
        }
        assert (mp->ma_used == 0);
        assert (mp->ma_table == mp->ma_smalltable);
        assert (mp->ma_mask == PyDict_MINSIZE - 1);
    } else {
        mp = PyObject_GC_New(PyDictObject, &PyDict_Type);
        if (mp == NULL)
            return NULL;
        EMPTY_TO_MINSIZE(mp);
    }
    mp->ma_lookup = lookdict_string;
    return (PyObject *)mp;
}

主要做了三件事:

  • 初始化dummy
  • 判断有无free_list,有的话从free_list拿mp,没有的话new一个mp,并重置
  • 设置ma_lookup

lookdict和lookdict_string

lookdict_string是针对string作为key的那些dict的,而lookdict则是一般化的。

区别在于比较字符串不会引发异常,而比较一般化的key可能会引发异常。

共同点是,如果找不到目标key,那么会返回一个entry对象,entry里的me_value为NULL,这个entry不是临时的,而是dict里对应这个key的entry。所以caller可以增加kv到这个entry里。

一个dict默认是用lookdict_string的,只有遇到lookdict_string的key参数不是string,才会提升到lookdict。

lookdict_string是比lookdict高效的,所以能用字符串当key就尽量用字符串。

观察通用的lookdict,发现有一个freeslot变量,它指向第一个dummy的entry,当lookdict找不到目标key对应的entry时,就会返回freeslot,如果没有freeslot,那么就返回第一个被发现的unused的entry。

插入元素

insert接口:

static int
insertdict_by_entry(register PyDictObject *mp, PyObject *key, long hash,
                    PyDictEntry *ep, PyObject *value)
{
    PyObject *old_value;

    MAINTAIN_TRACKING(mp, key, value);
    if (ep->me_value != NULL) { // 替换操作
        old_value = ep->me_value;
        ep->me_value = value;
        Py_DECREF(old_value); /* which **CAN** re-enter */
        Py_DECREF(key);
    }
    else {
        // 要么是unused,要么是dummy
        // unused的话,fill统计值+1
        // dummy的话,dummy对象引用-1
        if (ep->me_key == NULL)
            mp->ma_fill++;
        else {
            assert(ep->me_key == dummy);
            Py_DECREF(dummy);
        }
        ep->me_key = key; // 此时已经处理完me_key的引用计数了,可以直接赋值
        ep->me_hash = (Py_ssize_t)hash;
        ep->me_value = value;
        mp->ma_used++;
    }
    return 0;
}

调用insertdict_by_entry的主要入口:

static int
dict_set_item_by_hash_or_entry(register PyObject *op, PyObject *key,
                               long hash, PyDictEntry *ep, PyObject *value)
{
    register PyDictObject *mp;
    register Py_ssize_t n_used;

    mp = (PyDictObject *)op;
    assert(mp->ma_fill <= mp->ma_mask);  /* at least one empty slot */
    n_used = mp->ma_used;
    Py_INCREF(value);
    Py_INCREF(key);
    if (ep == NULL) {
        if (insertdict(mp, key, hash, value) != 0)
            return -1;
    }
    else {
        if (insertdict_by_entry(mp, key, hash, ep, value) != 0)
            return -1;
    }
    // 这里有个控制是否resize的规则:
    // 如果插入了新元素且填充率(fill / (mask+1))大于2/3 那么要resize
    if (!(mp->ma_used > n_used && mp->ma_fill*3 >= (mp->ma_mask+1)*2))
        return 0;
    // resize,新的size和used数有关
    // Note: 这里可能是发生收缩的
    return dictresize(mp, (mp->ma_used > 50000 ? 2 : 4) * mp->ma_used);
}

PySetObject

typedef struct _setobject PySetObject;
struct _setobject {
    PyObject_HEAD

    Py_ssize_t fill;  /* # Active + # Dummy */
    Py_ssize_t used;  /* # Active */

    /* The table contains mask + 1 slots, and that's a power of 2.
     * We store the mask instead of the size because the mask is more
     * frequently needed.
     */
    Py_ssize_t mask;

    /* table points to smalltable for small tables, else to
     * additional malloc'ed memory.  table is never NULL!  This rule
     * saves repeated runtime null-tests.
     */
    setentry *table;
    setentry *(*lookup)(PySetObject *so, PyObject *key, long hash);
    setentry smalltable[PySet_MINSIZE];

    long hash;                  /* only used by frozenset objects */
    PyObject *weakreflist;      /* List of weak references */
};

看定义,和dict是很像的。

PyTupleObject

typedef struct {
    PyObject_VAR_HEAD
    PyObject *ob_item[1];

    /* ob_item contains space for 'ob_size' elements.
     * Items must normally not be NULL, except during construction when
     * the tuple is not yet visible outside the function that builds it.
     */
} PyTupleObject;

tuple的PyTuple_New仅分配空间,这是因为tuple是不可变长的对象,长度一开始就知道了:

PyObject *
PyTuple_New(register Py_ssize_t size);

奇怪的是,如果有初始化列表,是执行abstract.c里的PySequence_Tuple来创建tuple,参数就是初始化列表。

set的特性是key不重复(tuple也是基于dict的实现)。

tuple元素的长度、获取和设置,基于宏和下标访问:

/* Macro, trading safety for speed */
#define PyTuple_GET_ITEM(op, i) (((PyTupleObject *)(op))->ob_item[i])
#define PyTuple_GET_SIZE(op)    Py_SIZE(op)

/* Macro, *only* to be used to fill in brand new tuples */
#define PyTuple_SET_ITEM(op, i, v) (((PyTupleObject *)(op))->ob_item[i] = v)

当然在python解释器里是不能修改tuple的元素的。

内存管理策略

第一层

相关文件:

  • pymem.h

这一层的名字叫PYMEM。

这个文件定义了最底层的内存管理接口:

  • PyMem_MALLOC: 实际调用C的malloc
  • PyMem_REALLOC: 实际调用C的realloc
  • PyMem_FREE: 实际调用C的free

C的接口可以认为是第0层。

这3个接口的目标是做到平台无关,加了各种参数检查。

#define PyMem_MALLOC(n)     ((size_t)(n) > (size_t)PY_SSIZE_T_MAX ? NULL \
                : malloc(((n) != 0) ? (n) : 1))
#define PyMem_REALLOC(p, n) ((size_t)(n) > (size_t)PY_SSIZE_T_MAX  ? NULL \
                : realloc((p), ((n) != 0) ? (n) : 1))
#define PyMem_FREE      free

有了这3个后,又定义了面向对象的接口:

#define PyMem_New(type, n) \
  ( ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL :  \
    ( (type *) PyMem_Malloc((n) * sizeof(type)) ) )
#define PyMem_NEW(type, n) \
  ( ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL :  \
    ( (type *) PyMem_MALLOC((n) * sizeof(type)) ) )

new接口很适合用来做数组的内存分配,例如这样:

unsigned int *blocks = PyMem_New(unsigned int, len);

第二层

相关文件:

  • objimpl.h
  • obmalloc.c

这一层的名字叫PYMALLOC。

这一层关于内存管理的是这么三个接口:

PyAPI_FUNC(void *) PyObject_Malloc(size_t);
PyAPI_FUNC(void *) PyObject_Realloc(void *, size_t);
PyAPI_FUNC(void) PyObject_Free(void *);

#define PyObject_MALLOC         PyObject_Malloc
#define PyObject_REALLOC        PyObject_Realloc
#define PyObject_FREE           PyObject_Free

这3个接口的实现很复杂,并不能直接理解。而是得先理解更基础的几个东西:block、pool、arena。

block

  • block按8字节内存对齐 (ALIGNMENT)
  • 按照单block的size,总共有64种block,size分别是8、16、24、···、512字节,sizeClassIdx分别是0、1、···、63 (INDEX2SIZE)
  • 小于等于512字节的内存分配请求,都是用block(SMALL_REQUEST_THRESHOLD)

实际上block是个抽象的概念,通过有限的几个宏表达了这个概念。没有很具体的地方,所以直接看pool即可。

pool

基本特点:

  • pool是block的集合
  • 每个pool大小都是4KB
  • pool里的block的大小取决于szidx

首先是POOL_SIZE的定义:

#define SYSTEM_PAGE_SIZE        (4 * 1024)
#define SYSTEM_PAGE_SIZE_MASK   (SYSTEM_PAGE_SIZE - 1)
#define POOL_SIZE               SYSTEM_PAGE_SIZE        /* must be 2^N */
#define POOL_SIZE_MASK          SYSTEM_PAGE_SIZE_MASK

可见,POOL_SIZE等于系统页大小,默认4KB。

然后是pool_header:

/* Pool for small blocks. */
struct pool_header {
    union { block *_padding; // 没有被使用的,只是为了让count也是按8字节对齐
            uint count; } ref;  // 每次取用freeblock加1,在PyObject_Free里可能会减1
    block *freeblock;
    struct pool_header *nextpool;       
    struct pool_header *prevpool;       
    uint arenaindex;                   
    uint szidx;                         
    uint nextoffset;  // 相当于指向pool内的下一个未曾用过的block的地址
    uint maxnextoffset;  // nextoffset的最大有效值
};

typedef struct pool_header *poolp;
  • pool是一块4KB的内存块,pool_header是这块内存块的地址头。
  • 每个pool会对应某一种类型的block,这是通过pool_header的szidx来对应的。
  • pool的初始化代码在init_pool里
  • maxnextoffset,实际上可以基于szidx实时计算得到,缓存在header里是为了性能考虑

nextoffset的唯一用途是获得freeblock地址:

if (pool->nextoffset <= pool->maxnextoffset) {
    //nextoffset还没超出maxnextoffset,说明还有空闲block
    pool->freeblock = (block*)pool +
                        pool->nextoffset;
    // 继续指向下一个偏移地址(可能会超过maxnextoffset)
    pool->nextoffset += INDEX2SIZE(size);
    // 这一行就比较迷了
    *(block **)(pool->freeblock) = NULL;
    UNLOCK();
    return (void *)bp;
}

(注意,nextoffset是只增不减的)

nextpool、prevpool字段则是用来把同类型的pool串成链表的(链表头暂时先不表)。 例如,当pool->nextoffset > pool->maxnextoffset时,会执行下面的代码,把这个pool从链表中移除:

    /* Pool is full, unlink from used pools. */
    next = pool->nextpool;
    pool = pool->prevpool;
    next->prevpool = pool;
    pool->nextpool = next;

pool有三个状态:

  • used: pool中至少有一个block被使用,且至少有一个block未使用
  • full: pool中所有block都被使用了
  • empty:pool中所有block都未被使用

arena

基本特点:

  • arena是pool的集合
  • arena数量是否有限看三个宏:WITH_MEMORY_LIMITS、SMALL_MEMORY_LIMIT、MAX_ARENAS,默认无限制
  • arena里的各个pool虽然都是4KB,但pool的szidx不一定一样
  • 单个arena默认为256KB

先看arena_object:

/* Record keeping for arenas. */
struct arena_object {
    uptr address;

    block* pool_address; // 指向这个arena的第一个pool

    uint nfreepools; // 可用pool数 free的 + 未分配的

    uint ntotalpools; // 总pool数

    struct pool_header* freepools; // 可用free pool的单向链表

    struct arena_object* nextarena;
    struct arena_object* prevarena;
};

arena_object的实例不是随便存放的,而是用一个数组来组织:

static struct arena_object* arenas = NULL;
static uint maxarenas = 0;
// 初始化默认创建16个arena_object 总共占16*256KB = 4MB内存
#define INITIAL_ARENA_OBJECTS 16

new_arena函数,是唯一会修改arenas地址的地方,当unused_arena_objects == NULL时会执行以下代码:

        // numarenas表示总共要分配多少个arena,最后会存进maxarenas
        // 每次翻一番
        numarenas = maxarenas ? maxarenas << 1 : INITIAL_ARENA_OBJECTS;
        if (numarenas <= maxarenas)
            return NULL;                // 溢出判断
#if SIZEOF_SIZE_T <= SIZEOF_INT
        if (numarenas > PY_SIZE_MAX / sizeof(*arenas))
            return NULL;                // 溢出判断
#endif
        nbytes = numarenas * sizeof(*arenas); // arenas数组总共占多少字节
        arenaobj = (struct arena_object *)realloc(arenas, nbytes);
        if (arenaobj == NULL)
            return NULL;
        arenas = arenaobj;// 指向该内存块首地址
        ···
        maxarenas = numarenas;

···省略的是arenas内部的初始化逻辑,目的是让新分配的arena_object之间串成链表,并让unused_arena_objects指向这些新对象的第一个:

    for (i = maxarenas; i < numarenas; ++i) {
        arenas[i].address = 0; 
        arenas[i].nextarena = i < numarenas - 1 ?
                                &arenas[i+1] : NULL;
    }
    unused_arena_objects = &arenas[maxarenas];

arena_object有2种状态:

  • unused:arena_object未关联内存块。用unused_arena_objects组织成单向链表。表头arena_object在new_arena被取出,在PyObject_Free被放回。简单地说,当address字段为0时,arena_objectt就在unused链表里
  • usable:arena_object已关联内存块。用usable_arenas组织成双向链表。这个链表是有排序的,按照nfreepools大小,升序,意味着下一次内存分配,会优先使用nfreepools最小的。这就使得nfreepools最大的arena有机会被清空,并把内存归还给系统。

arena_object.png

下面看new_arena中unused_arena_objects的取用:

    assert(unused_arena_objects != NULL);
    // 表头arena_object被取出
    arenaobj = unused_arena_objects;
    unused_arena_objects = arenaobj->nextarena; // 指向链表下一节点,可能为NULL
    assert(arenaobj->address == 0); //  unused链表的arenaobj必然是没有关联内存块的
    address = malloc(ARENA_SIZE); // 分配内存 256KB
    err = (address == 0);
    ···
    arenaobj->address = (uptr)address;


    arenaobj->freepools = NULL; // 经过free回收的pool,初始当然是null
    arenaobj->pool_address = (block*)arenaobj->address;
    arenaobj->nfreepools = ARENA_SIZE / POOL_SIZE;
    assert(POOL_SIZE * arenaobj->nfreepools == ARENA_SIZE);
    excess = (uint)(arenaobj->address & POOL_SIZE_MASK);
    if (excess != 0) {// 处理对齐,会导致实际少一个pool(部分空间是浪费掉的)
        --arenaobj->nfreepools;
        arenaobj->pool_address += POOL_SIZE - excess;
    }
    arenaobj->ntotalpools = arenaobj->nfreepools;

    return arenaobj;

unused_arena_objects的分析到此为止。下面是usable_arenas,在理解usable_arenas之前需要先理解usedpool。

usedpool面向一个需求:用户给定一个内存分配需求nbytes,怎么从arena、pool这些东西里得到合适的内存块?

首先第一点是,arena没有size属性,arena只是pool的集合。所以不能直接请求一个nbytes的arena。因此,应该是寻找一个pool。

前面说过pool有三种状态,现在可以深入分析下:

  • used: pool中至少有一个block被使用,且至少有一个block未使用。这种pool会被usedpools数组控制。
  • full: pool中所有block都被使用了。full状态的pool并不会组织成链表。
  • empty:pool中所有block都未被使用。这种pool的集合,它们之间通过pool_header的nextpool组织成一个链表,链表表头就是arena_object的freepools

当申请内存时,入手点是usedpools,在其中找到和nbytes匹配的pool,并从中分配一个block。

这里面的玄机非常复杂,先搬出usedpools观摩下:

#define ALIGNMENT               8               /* must be 2^N */
#define ALIGNMENT_SHIFT         3
#define ALIGNMENT_MASK          (ALIGNMENT - 1)
// NB_SMALL_SIZE_CLASSES等于64         ( 512 / 8 )
#define NB_SMALL_SIZE_CLASSES   (SMALL_REQUEST_THRESHOLD / ALIGNMENT)

#define PTA(x)  ((poolp )((uchar *)&(usedpools[2*(x)]) - 2*sizeof(block *)))
#define PT(x)   PTA(x), PTA(x)

static poolp usedpools[2 * ((NB_SMALL_SIZE_CLASSES + 7) / 8) * 8] = {
    PT(0), PT(1), PT(2), PT(3), PT(4), PT(5), PT(6), PT(7)
    , PT(8), PT(9), PT(10), PT(11), PT(12), PT(13), PT(14), PT(15)
    , PT(16), PT(17), PT(18), PT(19), PT(20), PT(21), PT(22), PT(23)
    , PT(24), PT(25), PT(26), PT(27), PT(28), PT(29), PT(30), PT(31)
    , PT(32), PT(33), PT(34), PT(35), PT(36), PT(37), PT(38), PT(39)
    , PT(40), PT(41), PT(42), PT(43), PT(44), PT(45), PT(46), PT(47)
    , PT(48), PT(49), PT(50), PT(51), PT(52), PT(53), PT(54), PT(55)
    , PT(56), PT(57), PT(58), PT(59), PT(60), PT(61), PT(62), PT(63)
};

usedpools总共有128个元素PT(0)、PT(1)、···、PT(63),或者说是PTA(0)、PTA(0)、PTA(1)、PTA(1)、···、PTA(63)、PTA(63)。每相邻的2个元素的值是一样的。

i of usedpools: 0 1 2 3 4 5 6 7 8 9

x of PTA:         0 0 1 1 2 2 3 3 4 4

以PT(5)为例:

PT(5) <=> PTA(5), PTA(5) <=> usedpools[10],usedpools[11]

usedpools[10] = PTA(5) = &usedpools[10] - 2

usedpools[11] = PTA(5) = &usedpools[10] - 2

也就是说,PT(5)使得usedpools[10]和usedpools[11]存了(&usedpools[10] - 2)的地址。

更一般地说,PT(x)使得usedpools[2 * x]和usedpools[2 * x + 1]存了(&usedpools[2 * x] - 2)的地址。

这么折腾,是和pool_header有关:

struct pool_header { // (&usedpools[2 * x] - 2)
    union { block *_padding;
            uint count; } ref;     
    block *freeblock; 
    struct pool_header *nextpool; // 这个就是 usedpools[2 * x]
    struct pool_header *prevpool; // 这个就是 usedpools[2 * x + 1]
    ···
};

也就是每2个usedpools中的元素,都相当于nextpool和prevpool指针,2个元素的值都存了对应的“pool_header"的地址。

usedpools是next、prev的集合,也是各个pool的双向链表的头节点

usedpools初始结构先理解到这里,现在要先看看usedpools是怎么使用的。

假设要分配的内存大小是28字节,那么第一步是算出sizeClassIdx:

// 在 void * PyObject_Malloc(size_t nbytes)
size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;
// 即 size class idx = (28 - 1) >> 3 = 3

下一行是查询usedpools数组了:

pool = usedpools[size + size];

前面已经分析过了usedpools, 每个usedpools的元素的值其实都是一个地址(指向对应的pool_header)。所以可以推知:

pool = usedpools[3 + 3] = PTA(3) = &usedpools[2*3] - 2

(&usedpools[2*sizeClassIdx] - 2)就是聚合了所有处于used态的、对应这个sizeClassIdx的pools链表的首个pool。

所以这一行操作,就是为了拿到目标used链表的表头。

(纵观usedpools的所有调用代码,都是usedpools[size + size]这样的调用)

接着下一行代码是:

if (pool != pool->nextpool) {
}

第一次分配时,pool和pool->nextpool是相等的(usedpools的初始值),并不会进入这个if。换句话说,就是没有处于used状态的对应这个sizeClassIdx的pool。

跳过这个if后,接着会new一个arena,然后获取它的freepool:

pool = usable_arenas->freepools;

然后直接进入init_pool逻辑段:

init_pool:
    /* Frontlink to used pools. */
    next = usedpools[size + size];
    pool->nextpool = next;
    pool->prevpool = next;
    next->nextpool = pool;
    next->prevpool = pool;
    ···
    return (void *)bp;
}

这4行骚操作,是在维护一个used双向链表,且是把pool插入了next(next含义是头节点)的nextpool(叫frontlink),当然这里因为已知链表是空的,所以nextpool也是prevpool。

在PyObject_Free里有一个是插在nex他的prev:

    --pool->ref.count;
    size = pool->szidx;
    next = usedpools[size + size];
    prev = next->prevpool;
    /* insert pool before next:   prev <-> pool <-> next */
    pool->nextpool = next;
    pool->prevpool = prev;
    next->prevpool = pool;
    prev->nextpool = pool;

这是把pool插在next的prevpool位置。

然后是移除操作,在PyObject_Free里:

pool = POOL_ADDR(p);
/* Pool is now empty:  unlink from usedpools, and
* link to the front of freepools.  This ensures that
* previously freed pools will be allocated later
* (being not referenced, they are perhaps paged out).
*/
next = pool->nextpool;
prev = pool->prevpool;
next->prevpool = prev;
prev->nextpool = next;

很经典的双向链表移除节点操作。

翻译下usedpools的注释:

/*

Pool table -- headed, circular, doubly-linked lists of partially used pools.
Pool table就是usedpools,它是有头部的、有环的、双向链接的部分使用pools。

This is involved.  For an index i, usedpools[i+i] is the header for a list of
all partially used pools holding small blocks with "size class idx" i. So
usedpools[0] corresponds to blocks of size 8, usedpools[2] to blocks of size
16, and so on:  index 2*i <-> blocks of size (i+1)<<ALIGNMENT_SHIFT.
对于一个索引i,usedpools[i+i]是,聚合了所有处于used态的、对应i这个sizeClassIdx的
pools链表的头节点。例如,usedpools[0]对应size8的blocks,usedpools[2]对应size16
的blocks。
用公式表示这种对应关系:index 2*i <-> blocks of size (i+1)<<ALIGNMENT_SHIFT


Pools are carved off an arena's highwater mark (an arena_object's pool_address
member) as needed.  Once carved off, a pool is in one of three states forever
after:
有需要时,pools会从arena里割除(carved off),当被割除时,这个pool处于以下三个状态之一:

used:上文已经介绍过了。

full: 当pool变成full时(没有freeblock),会从usedpools中移除,并且poolnextpool、
prevpool指针会变得无意义。

empty: 这个状态也会导致把pool从usedpools中移除。
*/

引用计数与GC

C++也有引用计数,不过C++没有GC机制,计数降为0就立即回收内存,且C++不会自动处理循环引用,避免在C++中出现循环引用的办法是使用weak指针,且需要程序员自己管理好指针才行。

而python的循环引用,是通过GC来处理的,并不需要什么weak指针(不过python有一个weakref模块可以实现弱引用)。

基本概念

  • 循环引用出现在容器类的对象之间
  • GC只需要考虑这些容器对象,而不用操心int、string之类的对象
  • 为了跟踪这些容器对象,python用一个双向链表来跟踪这些容器对象,有一定代价
  • 如果没有循环引用,那么不需要有GC这套东西,引用计数就足以处理对象释放问题了
  • GC就是为了循环引用问题而存在的

支持GC的对象的创建

这种对象都得用PyObject_GC_New来创建,否则就跟踪不到了。

这个函数定义如下:

// objimpl.h
#define PyObject_GC_New(type, typeobj) \
                ( (type *) _PyObject_GC_New(typeobj) )
#define PyObject_GC_NewVar(type, typeobj, n) \
                ( (type *) _PyObject_GC_NewVar((typeobj), (n)) )


// gcmodule.c

// 非var的
PyObject *
_PyObject_GC_New(PyTypeObject *tp)
{
    PyObject *op = _PyObject_GC_Malloc(_PyObject_SIZE(tp));
    if (op != NULL)
        op = PyObject_INIT(op, tp);
    return op;
}

// var的
PyVarObject *
_PyObject_GC_NewVar(PyTypeObject *tp, Py_ssize_t nitems)
{
    const size_t size = _PyObject_VAR_SIZE(tp, nitems);
    PyVarObject *op = (PyVarObject *) _PyObject_GC_Malloc(size);
    if (op != NULL)
        op = PyObject_INIT_VAR(op, tp, nitems);
    return op;
}

其中重点的_PyObject_GC_Malloc:

PyObject *
_PyObject_GC_Malloc(size_t basicsize)
{
    PyObject *op;
    PyGC_Head *g;
    if (basicsize > PY_SSIZE_T_MAX - sizeof(PyGC_Head))
        return PyErr_NoMemory();
    g = (PyGC_Head *)PyObject_MALLOC(
        sizeof(PyGC_Head) + basicsize);
    if (g == NULL)
        return PyErr_NoMemory();
    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;
    }
    op = FROM_GC(g);
    return op;
}

出现一个未知的结构PyGC_Head:

/* GC information is stored BEFORE the object structure. */
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 at least 8-byte alignment. */
    char dummy_padding[sizeof(union _gc_head_old)];
} PyGC_Head;

忽略那些对齐的东西,其实就是让这个object多了几个字段,用来做GC。gc_next和gc_prev是通过_PyObject_GC_TRACK和_PyObject_GC_UNTRACK控制的,这2个接口在创建完对象、即将返回对象前才调用,因为要确保被track的对象真的被成功创建了。gc_refs下文再解释。

上面的还有别的东西要注意:

_PyObject_GC_TRACK永远都是把对象放进0代的链表,而_PyObject_GC_UNTRACK则是把对象从它所属的代链表中移除,注意移除的时候不一定是0代了,可能是1、2。

用来做指针转换的宏:

/* Get an object's GC head */
#define AS_GC(o) ((PyGC_Head *)(o)-1)
/* Get the object given the GC head */
#define FROM_GC(g) ((PyObject *)(((PyGC_Head *)g)+1))

enabled和collecting全局变量:

// 是否开启自动垃圾收集
static int enabled = 1;
// 实际是bool变量,用来表示是否正在收集垃圾
static int collecting = 0;

标记-清除和分代GC

分代GC

代:

将系统中的所有内存块根据其存活时间划分为不同的集合,每一个集合就称为一个“代”,垃圾收集的频率随着“代”的存活时间的增大而减小,也就是说,活得越长的对象,就越可能不是垃圾,就应该越少去收集。那么这个存活时间是如何来衡量的呢,通常是利用经过了几次垃圾收集动作来衡量,如果一个对象经过的垃圾收集次数越多,那么显然,其存活时间就越长。

和代有关的代码:

/*** Global GC state ***/

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

#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 */
    ,           700,            0},
    ,           10,             0},
    ,           10,             0},
};

PyGC_Head *_PyGC_generation0 = GEN_HEAD(0);

定义了三个代,阈值分别为700、10、10.

注意count的含义有2种:

  • 0代的count:属于该代的对象的数量
  • 1、2代的count:上一代(1的上一代是0,2的上一代是1)已经collect了多少次,不是对象数量了

collect_generations

static Py_ssize_t
collect_generations(void)
{
    int i;
    Py_ssize_t n = 0;

    // 从最老的代遍历到最年轻的代,如果发现哪一代的count大于threshold
    // 那么收集<=i代的对象
    for (i = NUM_GENERATIONS-1; i >= 0; i--) {
        if (generations[i].count > generations[i].threshold) {
            // 第i代的count已经超过阈值了

            // (long_lived_pending/long_lived_total) < 25%时,是不做回收的
            // 这是为了减少回收调用次数,从而尽量减少GC开销
            // pending是指回收第i=1代时,转入i=2代的对象的数量的累加
            // total是指回收第i=2代时,当前2代的对象数量,更新total时会同时把pending置0
            if (i == NUM_GENERATIONS - 1
                && long_lived_pending < long_lived_total / 4)
                continue;
            n = collect(i);
            break;
        }
    }
    return n;
}

root object

root object是不能被删除的对象,被root object引用到的对象也是不能删除的。

gc_ref

有效引用计数:例如循环引用的2个对象,引用计数都为1,但有效引用计数都为0。

计算有效引用计数的办法是,从每个对象出发,把它引用的对象的计数都减1,例如上面的2个互相引用的对象,就都会变0。

但有个问题:如果减完发现不能变为0,那么意味着有效引用计数大于0,这个对象就不能删除,因此被它引用的对象也不能删除但可能引用计数被减到0了。

于是,需要另外弄一个gc_ref来做这件事情:

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 = Py_REFCNT(FROM_GC(gc));
        assert(gc->gc.gc_refs != 0);
    }
}

update_refs,遍历链表的每个对象,把ob_refcnt复制到gc_refs,以备后续的subtract_refs使用:

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);
    }
}

也是遍历同一个链表,但这次是取出每个对象的tp_traverse函数指针,并执行:

traverse(FROM_GC(gc), (visitproc)visit_decref, NULL);

意思是遍历该对象引用的所有对象,对每个对象调用visit_decref,附加参数为NULL:

static int
visit_decref(PyObject *op, void *data)
{
    assert(op != NULL);
    if (PyObject_IS_GC(op)) {
        PyGC_Head *gc = AS_GC(op);
        assert(gc->gc.gc_refs != 0);
        if (gc->gc.gc_refs > 0)
            gc->gc.gc_refs--;
    }
    return 0;
}

很简单,判断是不是有GC head,有的话就可以对gc_refs做-1操作。

看一个dict的traverse例子:

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)) {
        Py_VISIT(pk);
        Py_VISIT(pv);
    }
    return 0;
}

也就这么回事,利用dict的迭代机制,拿到所有pk、pv,执行Py_VISIT:

// 这个东西暴力地默认了变量名,这是为了统一所有traverse接口的命名
#define Py_VISIT(op)                                                    \
    do {                                                                \
        if (op) {                                                       \
            int vret = visit((PyObject *)(op), arg);                    \
            if (vret)                                                   \
                return vret;                                            \
        }                                                               \
    } while (0)

执行完subtract_refs,就完成了循环引用的摘除,链表里的对象如果还有gc_ref大于0,那么就是不可删除对象了。

接着就是开始做标记-清除:

    gc_list_init(&unreachable);
    move_unreachable(young, &unreachable);

这2行意图是把链表(young就是链表头)中的不可达对象都放进unreachable链表里,之后young里面就都是可达对象了。

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 = Py_TYPE(op)->tp_traverse;
            assert(gc->gc.gc_refs > 0);
            // 引用计数大于0,必然是可达对象
            gc->gc.gc_refs = GC_REACHABLE;
            // 这个traverse可能会修改young链表
            (void) traverse(op,
                            (visitproc)visit_reachable,
                            (void *)young);
            next = gc->gc.gc_next;
            if (PyTuple_CheckExact(op)) {
                _PyTuple_MaybeUntrack(op);
            }
        }
        else {
            next = gc->gc.gc_next;
            gc_list_move(gc, unreachable);
            // gc_refs等于0不一定就是不可达,因为可能是只有一个root object引用了它
            // 于是标记为”暂时“不可达
            gc->gc.gc_refs = 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 = gc->gc.gc_refs;

        if (gc_refs == 0) { 
            // 这种对象就是已经在young链表,但是move_unreachable函数还没
            // 遍历到它
            // 因为被visit的对象,都是可达对象引用到的对象,所以此时可以
            // 提前把它标记为可达的,即gc_refs改为大于0的值
            gc->gc.gc_refs = 1;
        }
        else if (gc_refs == GC_TENTATIVELY_UNREACHABLE) {
            // 这种对象是已经被move_unreachable遍历到了,此时和第一个if
            // 一样,标记为可达,并重新放进reachable(young)链表
            // move_unreachable会再次遍历到这个对象
            gc_list_move(gc, reachable);
            gc->gc.gc_refs = 1;
        } else { // 这个else就相当于bug检测了
            assert(gc_refs > 0
                   || gc_refs == GC_REACHABLE
                   || gc_refs == GC_UNTRACKED);
         }
    }
    return 0;
}

最终的,利用unreachable链表,就可以回收垃圾了:delete_garbage(&unreachable, old)


/* Break reference cycles by clearing the containers involved.  This is
 * tricky business as the lists can be changing and we don't know which
 * objects may be freed.  It is possible I screwed something up here.
 */
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);

        assert(IS_TENTATIVELY_UNREACHABLE(op));
        if (debug & DEBUG_SAVEALL) {
            PyList_Append(garbage, op);
        }
        else {
            if ((clear = Py_TYPE(op)->tp_clear) != NULL) {
                Py_INCREF(op);
                clear(op);
                Py_DECREF(op);
            }
        }
        if (collectable->gc.gc_next == gc) {
            // 这2个指针仍然相等意味着销毁失败了
            // 放回去old列表认为是可达对象即可
            gc_list_move(gc, old);
            gc->gc.gc_refs = GC_REACHABLE;
        }
    }
}

销毁操作是精妙的三小步:

  • 增加引用计数
  • tp_clear(清空容器,使得容器不再引用任何对象)
  • 减引用计数

假设现在有1个循环引用的list:

a = []
a.append(a)
a = None

a = None后,这个列表对象[]就没有外部引用了。但[]里存了一个元素,这个元素是[]自己,于是就形成了循环引用,且[]的引用计数为1。

当delete_garbage开始处理这个[]时,第一步使得[]的引用计数+1,变成2,然后调用list_clear:

static int
list_clear(PyListObject *a)
{
    Py_ssize_t i;
    PyObject **item = a->ob_item;
    if (item != NULL) {
        /* Because XDECREF can recursively invoke operations on
           this list, we make it empty first. */
        i = Py_SIZE(a);
        Py_SIZE(a) = 0;
        a->ob_item = NULL;
        a->allocated = 0;
        while (--i >= 0) {
            Py_XDECREF(item[i]);
        }
        PyMem_FREE(item);
    }
    /* Never fails; the return value can be ignored.
       Note that there is no guarantee that the list is actually empty
       at this point, because XDECREF may have populated it again! */
    return 0;
}

list_clear遍历它的所有元素并使引用计数-1,因为[]里只有一个元素,且是[]本身,于是list_clear之后,[]里就没有任何元素了,且引用计数降为1。

第三步,又减了一次[]的引用计数,此时引用计数终于降为0,这会导致立即销毁了[]本身,也导致[]从它所属的generation链表中被移除。

垃圾回收就这么完成了。

测试gc

import gc
import sys

class T:
    def __init__(self):
        self.me = self 
    def __del__(self):
        pass

def generate():
    for i in xrange(0, 1000000):
        a = T()
        # print(sys.getrefcount(a)) 输出 3
        a = None


gc.set_debug(gc.DEBUG_STATS|gc.DEBUG_LEAK)
raw_input("press to gc.collect again")
gc.collect()
raw_input("press to gc.collect again")
gc.collect()
raw_input("press to gc.collect again")
gc.collect()
raw_input("press to gc.collect again")
gc.collect()
gc.set_debug(0)


raw_input("press to generate garbages")
print("running...")
generate()
print("done")

gc.set_debug(gc.DEBUG_STATS|gc.DEBUG_LEAK)
raw_input("press to gc.collect again")
gc.collect()
raw_input("press to gc.collect again")
gc.collect()
raw_input("press to gc.collect again")
gc.collect()
gc.set_debug(0)

raw_input("press to exit")
print("exit")

输出:

$python test_gc.py 
press to gc.collect again
gc: collecting generation 2...
gc: objects in each generation: 663 3148 0
gc: done, 0.0014s elapsed.
press to gc.collect again
gc: collecting generation 2...
gc: objects in each generation: 0 0 3442
gc: done, 0.0013s elapsed.
press to gc.collect again
gc: collecting generation 2...
gc: objects in each generation: 0 0 3440
gc: done, 0.0014s elapsed.
press to gc.collect again
gc: collecting generation 2...
gc: objects in each generation: 0 0 3440
gc: done, 0.0013s elapsed.
press to generate garbages
running...
done
press to gc.collect again
gc: collecting generation 2...
gc: objects in each generation: 38 7710 1995692
gc: uncollectable <T instance at 0x11891e200>
gc: uncollectable <T instance at 0x11891e248>
gc: uncollectable <T instance at 0x11891e290>
gc: uncollectable <T instance at 0x11891e2d8>
gc: uncollectable <T instance at 0x11891e320>
gc: uncollectable <T instance at 0x11891e368>
gc: uncollectable <T instance at 0x11891e3b0>
gc: uncollectable <T instance at 0x11891e3f8>
gc: uncollectable <T instance at 0x11891e440>
gc: uncollectable <T instance at 0x11891e488>
gc: uncollectable <T instance at 0x11891e4d0>
gc: uncollectable <T instance at 0x11891e518>
gc: uncollectable <T instance at 0x11891e560>
gc: uncollectable <T instance at 0x11891e5a8>
gc: uncollectable <T instance at 0x11891e5f0>
gc: uncollectable <T instance at 0x11891e638>
gc: uncollectable <T instance at 0x11891e680>
gc: uncollectable <T instance at 0x11891e6c8>
gc: uncollectable <T instance at 0x11891e710>
gc: uncollectable <dict 0x11891f280>
gc: uncollectable <dict 0x11891f398>
gc: uncollectable <dict 0x11891f4b0>
gc: uncollectable <dict 0x11891f5c8>
gc: uncollectable <dict 0x11891f6e0>
gc: uncollectable <dict 0x11891f7f8>
gc: uncollectable <dict 0x11891f910>
gc: uncollectable <dict 0x11891fa28>
gc: uncollectable <dict 0x11891fb40>
gc: uncollectable <dict 0x11891fc58>
gc: uncollectable <dict 0x11891fd70>
gc: uncollectable <dict 0x11891fe88>
gc: uncollectable <dict 0x118920050>
gc: uncollectable <dict 0x118920168>
gc: uncollectable <dict 0x118920280>
gc: uncollectable <dict 0x118920398>
gc: uncollectable <dict 0x1189204b0>
gc: uncollectable <dict 0x1189205c8>
gc: uncollectable <dict 0x1189206e0>
gc: done, 38 unreachable, 38 uncollectable, 0.2670s elapsed.
press to gc.collect again
gc: collecting generation 2...
gc: objects in each generation: 0 0 2003440
gc: done, 0.2749s elapsed.
press to gc.collect again
gc: collecting generation 2...
gc: objects in each generation: 0 0 2003440
gc: done, 0.2798s elapsed.
press to exit
exit

观察输出,可以发现generate()共产生了200万个不能被gc销毁的垃圾对象。

generate()的for循环总共循环了100万次,所以每次迭代,都产生了2个垃圾对象。

根据输出信息:

gc: uncollectable <T instance at 0x11891e710>
gc: uncollectable <dict 0x11891f280>

可确定,2个垃圾对象的其中一个是T的实例(PyInstanceObject),另一个是T实例的属性字典(PyInstanceObject.in_dict)。

T实例因为有del,has_finalizer会返回true,于是被放进finializer链表:

static void
move_finalizers(PyGC_Head *unreachable, PyGC_Head *finalizers)
{
    PyGC_Head *gc;
    PyGC_Head *next;
    for (gc = unreachable->gc.gc_next; gc != unreachable; gc = next) {
        PyObject *op = FROM_GC(gc);

        assert(IS_TENTATIVELY_UNREACHABLE(op));
        next = gc->gc.gc_next;

        if (has_finalizer(op)) {
            gc_list_move(gc, finalizers);
            gc->gc.gc_refs = GC_REACHABLE;
        }
    }
}

move_finalizers之后会执行move_finalizer_reachable:

static void
move_finalizer_reachable(PyGC_Head *finalizers)
{
    traverseproc traverse;
    PyGC_Head *gc = finalizers->gc.gc_next;
    for (; gc != finalizers; gc = gc->gc.gc_next) {
        traverse = Py_TYPE(FROM_GC(gc))->tp_traverse;
        (void) traverse(FROM_GC(gc),
                        (visitproc)visit_move,
                        (void *)finalizers);
    }
}

move_finalizers把一些对象放进了finalizers链表并且设为GC_REACHABLE,这就导致,这些对象引用的所有对象,也应该是GC_REACHABLE。

所以move_finalizer_reachable的作用是,遍历finalizer,把被引用的其他对象也放进finalizer列表,即使这些对象并没有del,如上面测试程序里的uncollectable的dict。

总结:

class T:
    def __init__(self):
        self.me = self 
    def __del__(self):
        pass

这样定义的一个T类,它的任意实例都会导致产生2个垃圾对象,一个是实例本身,一个是实例引用的in_dict。

所以写python程序切勿写出这样子的代码,不然GC都帮不了你。

(未经授权禁止转载)
Written on July 28, 2018

博主将十分感谢对本文章的任意金额的打赏^_^