《python解释器源码剖析》第17章--python的内存管理与垃圾回收

Posted 来自东方地灵殿的小提琴手

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《python解释器源码剖析》第17章--python的内存管理与垃圾回收相关的知识,希望对你有一定的参考价值。

17.0 序

内存管理,对于python这样的动态语言是至关重要的一部分,它在很大程度上决定了python的执行效率,因为在python的运行中会创建和销毁大量的对象,这些都设计内存的管理。同理python还提供了了内存的垃圾回收(GC,garbage collection),将开发者从繁琐的手动维护内存的工作中解放出来。这一章我们就来分析python的GC是如何实现的。

17.1 内存管理架构

在python中内存管理机制是分层次的,我们可以看成有四层,0 1 2 3。在最底层,也就是第0层是由操作系统提供的内存管理接口,比如C提供了malloc和free接口,这一层是由操作系统实现并且管理的,python不能干涉这一行为。从这一层往上,剩余的三层则都是由python实现并维护的。

第一层是python基于第0层操作系统管理接口包装而成的,这一层并没有在第0层上加入太多的动作,其目的仅仅是为python提供一层统一的raw memory的管理接口。这么做的原因就是虽然不同的操作系统都提供了ANSI C标准所定义的内存管理接口,但是对于某些特殊情况不同操作系统有不同的行为。比如调用malloc(0),有的操作系统会返回NULL,表示申请失败,但是有的操作系统则会返回一个貌似正常的指针, 但是这个指针指向的内存并不是有效的。为了最广泛的可移植性,python必须保证相同的语义一定代表着相同的运行时行为,为了处理这些与平台相关的内存分配行为,python必须要在C的内存分配接口之上再提供一层包装。

在python中,第一层的实现就是一组以PyMem_为前缀的函数族,下面来看一下。

//include.h
PyAPI_FUNC(void *) PyMem_Malloc(size_t size);
PyAPI_FUNC(void *) PyMem_Realloc(void *ptr, size_t new_size);
PyAPI_FUNC(void) PyMem_Free(void *ptr);

//obmalloc.c
void *
PyMem_Malloc(size_t size)
{
    /* see PyMem_RawMalloc() */
    if (size > (size_t)PY_SSIZE_T_MAX)
        return NULL;
    return _PyMem.malloc(_PyMem.ctx, size);
}

void *
PyMem_Realloc(void *ptr, size_t new_size)
{
    /* see PyMem_RawMalloc() */
    if (new_size > (size_t)PY_SSIZE_T_MAX)
        return NULL;
    return _PyMem.realloc(_PyMem.ctx, ptr, new_size);
}

void
PyMem_Free(void *ptr)
{
    _PyMem.free(_PyMem.ctx, ptr);
}

我们看到在第一层,python提供了类似于类似于C中malloc、realloc、free的语义。并且我们发现,比如PyMem_Malloc,如果申请的内存大小超过了PY_SSIZE_T_MAX直接返回NULL,并且调用了_PyMem.malloc,这个C中的malloc几乎没啥区别,但是会对特殊值进行一些处理。到目前为止,仅仅是分配了raw memory而已。其实在第一层,python还提供了面向对象中类型的内存分配器。

//pymem.h
#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)) ) )
#define PyMem_Resize(p, type, n) \\
  ( (p) = ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL :        \\
        (type *) PyMem_Realloc((p), (n) * sizeof(type)) )
#define PyMem_RESIZE(p, type, n) \\
  ( (p) = ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL :        \\
        (type *) PyMem_REALLOC((p), (n) * sizeof(type)) )
#define PyMem_Del               PyMem_Free
#define PyMem_DEL               PyMem_FREE

很明显,在PyMem_Malloc中需要程序员自行提供所申请的空间大小。然而在PyMem_New中,只需要提供类型和数量,python会自动侦测其所需的内存空间大小。

第一层所提供的内存管理接口的功能是非常有限的,如果创建一个PyLongObject对象,还需要做很多额外的工作,比如设置对象的类型参数、初始化对象的引用计数值等等。因此为了简化python自身的开发,python在比第一层更高的抽象层次上提供了第二层内存管理接口。在这一层,是一组以PyObject_为前缀的函数族,主要提供了创建python对象的接口。这一套函数族又被换做Pymalloc机制。因此在第二层的内存管理机制上,python对于一些内建对象构建了更高抽象层次的内存管理策略。而对于第三层的内存管理策略,主要就是对象的缓存机制。因此:

第0层:操作系统负责管理内存,python无权干预

第1层:仅仅对c中原生的malloc进行了简单包装

第2层:真正在python中发挥巨大作用,并且也是GC的藏身之处

第3层:缓冲池,比如小整数对象池等等。

下面我们就来对第二层内存管理机制进行剖析。

17.2 小块空间的内存池

在python中,很多时候申请的内存都是小块的内存,这些小块的内存在申请后很快又被释放,并且这些内存的申请并不是为了创建对象,所以并没有对象一级的内存池机制。这就意味着python在运行期间需要大量的执行malloc和free操作,导致操作系统在用户态和内核态之间进行切换,这将严重影响python的效率。所以为了提高执行效率,python引入了一个内存池机制,用于管理对小块内存的申请和释放,这就是之前说的Pymalloc机制,并且提供了pymalloc_allocpymalloc_reallocpymalloc_free三个接口。

整个小块内存的内存池可以视为一个层次结构,在这个层次结构中一共分为4层,从下至上分别是:block、pool、arena和内存池。并且block(雾)、pool、arena都是python代码中可以找到的实体,而最顶层的内存池只是一个概念上的东西,表示python对整个小块内存分配和释放行为的内存管理机制。

17.2.1 block

在最底层,block是一个确定大小的内存块。而python中,有很多种block,不同种类的block都有不同的内存大小,这个内存大小的值被称之为size class。为了在当前主流的32位平台和64位平台都能获得最佳性能,所有的block的长度都是8字节对齐的。

//obmalloc.c
#define ALIGNMENT               8               /* must be 2^N */
#define ALIGNMENT_SHIFT         3

同时,python为block的大小设定了一个上限,当申请的内存大小小于这个上限时,python可以使用不同种类的block满足对内存的需求;当申请的内存大小超过了这个上限,python就会将对内存的请求转交给第一层的内存管理机制,即PyMem函数族来处理。这个上限值在python中被设置为512,如果超过了这个值还是要经过操作系统临时申请的。

//obmalloc.c
#define SMALL_REQUEST_THRESHOLD 512
#define NB_SMALL_SIZE_CLASSES   (SMALL_REQUEST_THRESHOLD / ALIGNMENT)

根据SMALL_REQUEST_THRESHOLDALIGNMENT的限定,实际上我们可以由此得到不同种类的block和size class。block是以8字节对齐,那么每一个块的大小都是8的整倍数,最大不超过512

 * Request in bytes     Size of allocated block      Size class idx
 * ----------------------------------------------------------------
 *        1-8                     8                       0
 *        9-16                   16                       1
 *       17-24                   24                       2
 *       25-32                   32                       3
 *       33-40                   40                       4
 *       41-48                   48                       5
 *       49-56                   56                       6
 *       57-64                   64                       7
 *       65-72                   72                       8
 *        ...                   ...                     ...
 *      497-504                 504                      62
 *      505-512                 512                      63

因此当我们申请一个44字节的内存时,PyObject_Malloc会从内存池中划分一个48字节的block给我们。

另外在python中,block只是一个概念,在python源码中没有与之对应的实体存在。之前我们说对象,对象在源码中有对应的PyObject,列表在源码中则有对应的PyListObject,但是这里的block仅仅是概念上的东西,我们知道它是具有一定大小的内存,但是它并不与python源码里面的某个东西对应。但是,python提供了一个管理block的东西,也就是我们下面要分析的pool。

17.2.2 pool

一组block的集合成为一个pool,换句话说,一个pool管理着一堆具有固定大小的内存块(block)。事实上,pool管理者一大块内存,它有一定的策略,将这块大的内存划分为多个小的内存块。在python中,一个pool的大小通常是为一个系统内存页,也就是4kb。

//obmalloc.c
#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

虽然python没有为block提供对应的结构,但是提供了和pool相关的结构,我们来看看

//obmalloc.c
/* Pool for small blocks. */
struct pool_header {
    union { block *_padding;
            uint count; } ref;          /* 当然pool里面的block数量    */
    block *freeblock;                   /* 一个链表,指向下一个可用的block   */
    struct pool_header *nextpool;       /* 指向下一个pool  */
    struct pool_header *prevpool;       /* 指向上一个pool       ""        */
    uint arenaindex;                    /* 在area里面的索引 */
    uint szidx;                         /* block的大小(固定值?后面说)     */
    uint nextoffset;                    /* 下一个可用block的内存偏移量         */
    uint maxnextoffset;                 /* 最后一个block距离开始位置的距离     */
};

typedef struct pool_header *poolp;

我们刚才说了一个pool的大小在python中是4KB,但是从当前的这个pool的结构体来看,用鼻子想也知道吃不完4KB的内存。所以呀,这个结构体叫做pool_header,它仅仅一个pool的头部,除去这个pool_header,还剩下的内存才是维护的所有block的集合所占的内存。

我们注意到,pool_header里面有一个szidx,这就意味着pool里面管理的内存块大小都是一样的。也就是说,一个pool可能管理了20个32字节的block、也可能管理了20个64字节的block,但是不会出现管理了10个32字节的block加上10个64字节的block存在。每一个pool都和一个size联系在一起,更确切的说都和一个size class index联系在一起,表示pool里面存储的block都是多少字节的。这就是里面的域szidx存在的意义。

假设我们手里有一块4kb的内存,来看看python是如何将这块内存改造为一个管理32字节block的pool,并从中取出第一块pool的。

//obmalloc.c
#define POOL_OVERHEAD   _Py_SIZE_ROUND_UP(sizeof(struct pool_header), ALIGNMENT)


static int
pymalloc_alloc(void *ctx, void **ptr_p, size_t nbytes)
{
    block *bp;
    poolp pool;
    ...	
    //pool指向了一块4kb的内存
    init_pool:
        pool->ref.count = 1;
        ...	    	
        //设置pool的size class index    
        pool->szidx = size;
        //将size class index转换成size,比如:0->8, 1->16, 63->512
        size = INDEX2SIZE(size);
        //跳过用于pool_header的内存,并进行对齐
        bp = (block *)pool + POOL_OVERHEAD;
        //等价于pool->nextoffset = POOL_OVERHEAD+size+size
        pool->nextoffset = POOL_OVERHEAD + (size << 1);
        pool->maxnextoffset = POOL_SIZE - size;
        pool->freeblock = bp + size;
        *(block **)(pool->freeblock) = NULL;
        goto success;
    ...
success:
    UNLOCK();
    assert(bp != NULL);
    *ptr_p = (void *)bp;
    return 1;
}

最后的(void *)bp;就是指向从pool中取出的第一块block的指针。也就是说pool中第一块block已经被分配了,所以在ref.count中记录了当前已经被分配的block的数量,这时为1,特别需要注意的是,bp返回的实际上是一个地址,这个地址之后有将近4kb的内存实际上都是可用的,但是可以肯定申请内存的函数只会使用[bp, bp+size]这个区间的内存,这是由size class index可以保证的。改造成pool之后的4kb内存如图所示:

实线箭头是指针,但是虚线箭头则是偏移位置的形象表示。在nextoffset,maxnextoffset中存储的是相对于pool头部的偏移位置。

在了解初始化之后的pool的样子之后,可以来看看python在申请block时,pool_header中的各个域是怎么变动的。假设我们从现在开始连续申请5块28字节内存,由于28字节对应的size class index为3,所以实际上会申请5块32字节的内存。

//obmalloc.c
static int
pymalloc_alloc(void *ctx, void **ptr_p, size_t nbytes)
{
    if (pool != pool->nextpool) {
        /*
         * There is a used pool for this size class.
         * Pick up the head block of its free list.
         */
        //首先pool中block数自增1
        ++pool->ref.count;
        //这里的freeblock指向的是下一个可用的block的起始地址
        bp = pool->freeblock;
        assert(bp != NULL);
        if ((pool->freeblock = *(block **)bp) != NULL) {
            goto success;
        }
		
        //因此当再次申请32字节block时,只需要返回freeblock指向的地址就可以了。那么很显然,freeblock需要前进,指向下一个可用的block,这个时候nextoffset就现身了
        if (pool->nextoffset <= pool->maxnextoffset) {
            //当nextoffset小于等于maxoffset时候
            //freeblock等于当前block的地址 + nextoffset(下一个可用block的内存偏移量)
            //所以freeblock正好指向了下一个可用block的地址
            pool->freeblock = (block*)pool +
                              pool->nextoffset;
            //同理,nextoffset也要向前移动一个block的距离
            pool->nextoffset += INDEX2SIZE(size);
            //依次反复,即可对所有的block进行遍历。而maxnextoffset指明了该pool中最后一个可用的block距离pool开始位置的偏移
            //当pool->nextoffset > pool->maxnextoffset就意味着遍历完pool中的所有block了
            //再次获取显然就是NULL了
            *(block **)(pool->freeblock) = NULL;
            goto success;
        }
}

所以,申请、前进、申请、前进,一直重复着相同的动作,整个过程非常自然,也容易理解。但是我们发现,由于无论多少个block,这些block必须都是具有相同大小,导致一个pool中只能满足POOL_SIZE / size次对block的申请,这就让人不舒服。举个栗子,现在我们已经进行了5次连续32字节的内存分配,可以想象,pool中5个连续的block都被分配出去了。过了一段时间,程序释放了其中的第2块和第4块block,那么下一次再分配32字节的内存的时候,pool提交的应该是第2块,还是第6块呢?显然为了pool的使用效率,最好分配自由的第二块block。因此可以想象,一旦python运转起来,内存的释放动作将导致pool中出现大量的离散的自由block,python为了知道哪些block是被使用之后再次被释放的,必须建立一种机制,将这些离散自由的block组合起来,再次使用。这个机制就是所有的自由block链表,这个链表的关键就在pool_header中的那个freeblock身上。

//obmalloc.c
/* Pool for small blocks. */
struct pool_header {
    union { block *_padding;
            uint count; } ref;          /* 当然pool里面的block数量    */
    block *freeblock;                   /* 一个链表,指向下一个可用的block   */
    struct pool_header *nextpool;       /* 指向下一个pool  */
    struct pool_header *prevpool;       /* 指向上一个pool       ""        */
    uint arenaindex;                    /* 在area里面的索引 */
    uint szidx;                         /* block的大小(固定值?后面说)     */
    uint nextoffset;                    /* 下一个可用block的内存偏移量         */
    uint maxnextoffset;                 /* 最后一个block距离开始位置的距离     */
};

typedef struct pool_header *poolp;

刚才我们说了,当pool初始化完后之后,freeblock指向了一个有效的地址,也就是下一个可以分配出去的block的地址。然而奇特的是,当python设置了freeblock时,还设置了*freeblock。这个动作看似诡异,然而我们马上就能看到设置*freeblock的动作正是建立离散自由block链表的关键所在。目前我们看到的freeblock只是在机械地前进前进,因为它在等待一个特殊的时刻,在这个特殊的时刻,你会发现freeblock开始成为一个苏醒的精灵,在这4kb的内存上开始灵活地舞动。这个特殊的时刻就是一个block被释放的时刻。

//obmalloc.c

//基于地址P获得离P最近的pool的边界地址
#define POOL_ADDR(P) ((poolp)_Py_ALIGN_DOWN((P), POOL_SIZE))

static int
pymalloc_free(void *ctx, void *p)
{
    poolp pool;
    block *lastfree;
    poolp next, prev;
    uint size;

    pool = POOL_ADDR(p);
    //如果p不在pool里面,直接返回0
    if (!address_in_range(p, pool)) {
        return 0;
    }
    LOCK();
	
    //释放,那么ref.count就是势必大于0
    assert(pool->ref.count > 0);            /* else it was empty */
    *(block **)p = lastfree = pool->freeblock;
    pool->freeblock = (block *)p;
}


在释放block时,神秘的freeblock惊鸿一现,覆盖在freeblock身上的那层面纱就要被揭开了。我们知道,这是freeblock虽然指向了一个有效的pool里面的地址,但是*freeblock是为NULL的。假设这时候python释放的是block A,那么A中的第一个字节的值被设置成了当前freeblock的值,然后freeblock的值被更新了,指向了block A的首地址。就是这两个步骤,一个block被插入到了离散自由的block链表中,所以当第2块和第4块block都被释放之后,我们可以看到一个初具规模的离散自由block链表了。

到了这里,这条实现方式非常奇特的block链表被我们挖掘出来了,从freeblock开始,我们可以很容易的以freeblock = *freeblock的方式遍历这条链表,而当发现了*freeblock为NULL时,则表明到达了该链表(可用自由链表)的尾部了,那么下次就需要申请新的block了。

//obmalloc.c
static int
pymalloc_alloc(void *ctx, void *p)
{
        if (pool != pool->nextpool) {
        ++pool->ref.count;
        bp = pool->freeblock;
        assert(bp != NULL);
        //如果这里的条件不为真,表明离散自由链表中已经不存在可用的block了
        //如果可能,则会继续分配pool的nextoffset指定的下一块block
        if ((pool->freeblock = *(block **)bp) != NULL) {
            goto success;
        }

        /*
         * Reached the end of the free list, try to extend it.
         */
        if (pool->nextoffset <= pool->maxnextoffset) {
            ...
        }
}

但是如果连pool->nextoffset <= pool->maxnextoffset这个条件都不成立了呢?pool的大小有限制啊,如果我再想申请block的时候,没空间了怎么办?再来一个pool不就好了,所以多个block可以组合成一个集合,pool;那么多个pool也可以组合起来,就是我们下面介绍的arena。

17.2.3 arena

在python中,多个pool聚合的结果就是一个arena。上一节提到,pool的大小默认是4kb,同样每个arena的大小也有一个默认值。#define ARENA_SIZE (256 << 10) ,显然这个值默认是256KB,也就是ARENA_SIZE / POOL_SIZE = 64个pool的大小。

//obmalloc.c
struct arena_object {
    uintptr_t address;
    block* pool_address;
    uint nfreepools;
    uint ntotalpools;
    struct pool_header* freepools;
    struct arena_object* nextarena;
    struct arena_object* prevarena;
};

一个概念上的arena在python源码中就对应arena_object结构体,确切的说,arena_object仅仅是arena的一部分。就像pool_header仅仅是pool的一部分一样,一个完整的pool包括一个pool_header和透过这个pool_header管理着的block集合;一个完整的arena也包括一个arena_object和透过这个arena_object管理着的pool集合。

"未使用的"的arena和"可用"的arena

在arena_object结构体的定义中,我们看到了nextarena和prevarena这两个东西,这似乎意味着在python中会有一个或多个arena构成的链表,这个链表的表头就是arenas。呃,这种猜测实际上只对了一半,实际上,在python中确实会存在多个arena_object构成的集合,但是这个集合不够成链表,而是一个数组。数组的首地址由arenas来维护,这个数组就是python中的通用小块内存的内存池。另一方面,nextarea和prevarena也确实是用来连接arena_object组成链表的,咦,不是已经构成或数组了吗?为啥又要来一个链表。

我们曾说arena是用来管理一组pool的集合的,arena_object的作用看上去和pool_header的作用是一样的。但是实际上,pool_header管理的内存和arena_object管理的内存有一点细微的差别。pool_header管理的内存pool_header自身是一块连续的内存,但是arena_object与其管理的内存则是分离的:

咋一看,貌似没啥区别,不过一个是连着的,一个是分开的。但是这后面隐藏了这样一个事实:当pool_header被申请时,它所管理的内存也一定被申请了;但是当arena_object被申请时,它所管理的pool集合的内存则没有被申请。换句话说,arena_object和pool集合在某一时刻需要建立联系。

当一个arena的arena_object没有与pool建立联系的时候,这时的arena就处于"未使用"状态;一旦建立了联系,这时arena就转换到了"可用"状态。对于每一种状态,都有一个arena链表。"未使用"的arena链表表头是unused_arena_objects,多个arena之间通过nextarena连接,并且是一个单向的链表;而"可用的"arena链表表头是usable_arenas,多个arena之间通过nextarena、prevarena连接,是一个双向链表。

申请arena

在运行期间,python使用new_arena来创建一个arena,我们来看看它是如何被创建的。

//obmalloc.c

//arenas,多个arena组成的数组的首地址
static struct arena_object* arenas = NULL;

//当arena数组中的所有arena的个数
static uint maxarenas = 0;

//未使用的arena的个数
static struct arena_object* unused_arena_objects = NULL;

//可用的arena的个数
static struct arena_object* usable_arenas = NULL;

//初始化需要申请的arena的个数
#define INITIAL_ARENA_OBJECTS 16

static struct arena_object*
new_arena(void)
{	
    //arena,一个arena_object结构体对象
    struct arena_object* arenaobj;
    uint excess;        /* number of bytes above pool alignment */
	
    //[1]:判断是否需要扩充"未使用"的arena列表
    if (unused_arena_objects == NULL) {
        uint i;
        uint numarenas;
        size_t nbytes;
		
        //[2]:确定本次需要申请的arena_object的个数,并申请内存
        numarenas = maxarenas ? maxarenas << 1 : INITIAL_ARENA_OBJECTS;
        ...
        nbytes = numarenas * sizeof(*arenas);
        arenaobj = (struct arena_object *)PyMem_RawRealloc(arenas, nbytes);
        if (arenaobj == NULL)
            return NULL;
        arenas = arenaobj;
        ...
        /* Put the new arenas on the unused_arena_objects list. */
        //[3]:初始化新申请的arena_object,并将其放入"未使用"arena链表中
        for (i = maxarenas; i < numarenas; ++i) {
            arenas[i].address = 0;              /* mark as unassociated */
            arenas[i].nextarena = i < numarenas - 1 ?
                                   &arenas[i+1] : NULL;
        }

        /* Update globals. */
        unused_arena_objects = &arenas[maxarenas];
        maxarenas = numarenas;
    }

    /* Take the next available arena object off the head of the list. */
    //[4]:从"未使用"arena链表中取出一个"未使用"的arena
    assert(unused_arena_objects != NULL);
    arenaobj = unused_arena_objects;
    unused_arena_objects = arenaobj->nextarena;
    assert(arenaobj->address == 0);
    
    //[5]:申请arena管理的内存,这里我们说的arena指的是arena_object,简写了
    address = _PyObject_Arena.alloc(_PyObject_Arena.ctx, ARENA_SIZE);
    if (address == NULL) {
        /* The allocation failed: return NULL after putting the
         * arenaobj back.
         */
        arenaobj->nextarena = unused_arena_objects;
        unused_arena_objects = arenaobj;
        return NULL;
    }
    arenaobj->address = (uintptr_t)address;
	
    //调整个数
    ++narenas_currently_allocated;
    ++ntimes_arena_allocated;
    if (narenas_currently_allocated > narenas_highwater)
        narenas_highwater = narenas_currently_allocated;
    //[6]:设置poo集合的相关信息,这是设置为NULL
    arenaobj->freepools = NULL;
    /* pool_address <- first pool-aligned address in the arena
       nfreepools <- number of whole pools that fit after alignment */
    arenaobj->pool_address = (block*)arenaobj->address;
    arenaobj->nfreepools = ARENA_SIZE / POOL_SIZE;
    assert(POOL_SIZE * arenaobj->nfreepools == ARENA_SIZE);
    //将pool的起始地址调整为系统页的边界
    excess = (uint)(arenaobj->address & POOL_SIZE_MASK);
    if (excess != 0) {
        --arenaobj->nfreepools;
        arenaobj->pool_address += POOL_SIZE - excess;
    }
    arenaobj->ntotalpools = arenaobj->nfreepools;

    return arenaobj;
}

因此我们可以看到,python首先会检查当前"未使用"链表中是否还有"未使用"arena,检查的结果将决定后续的动作。

如果在"未使用"链表中还存在未使用的arena,那么python会从"未使用"arena链表中抽取一个arena,接着调整"未使用"链表,让它和抽取的arena断绝一切联系。然后python申请了一块256KB大小的内存,将申请的内存地址赋给抽取出来的arena的address。我们已经知道,arena中维护的是pool集合,这块256KB的内存就是pool的容身之处,这时候arena就已经和pool集合建立联系了。这个arena已经具备了成为"可用"内存的条件,该arena和"未使用"arena链表脱离了关系,就等着被"可用"arena链表接收了,不过什么时候接收呢?先别急

随后,python在代码的[6]处设置了一些arena用户维护pool集合的信息。需要注意的是,python将申请到的256KB内存进行了处理,主要是放弃了一些内存,并将可使用的内存边界(pool_address)调整到了与系统页对齐。然后通过arenaobj->freepools = NULL;将freepools设置为NULL,这不奇怪,基于对freeblock的了解,我们知道要等到释放一个pool时,这个freepools才会有用。最后我们看到,pool集合占用的256KB内存在进行边界对齐后,实际是交给pool_address来维护了。

回到new_arena中的[1]处,如果unused_arena_objects为NULL,则表明目前系统中已经没有"未使用"arena了,那么python首先会扩大系统的arena集合(小块内存内存池)。python在内部通过一个maxarenas的变量维护了存储arena的数组的个数,然后在[2]处将待申请的arena的个数设置为当然arena个数(maxarenas)的2倍。当然首次初始化的时候maxarenas为0,此时为16。

在获得了新的maxarenas后,python会检查这个新得到的值是否溢出了。如果检查顺利通过,python就会在[3]处通过realloc扩大arenas指向的内存,并对新申请的arena_object进行设置,特别是那个不起眼的address,要将新申请的address一律设置为0。实际上,这是一个标识arena是出于"未使用"状态还是"可用"状态的重要标记。而一旦arena(arena_object)和pool集合建立了联系,这个address就变成了非0,看代码的[6]处。当然别忘记我们为什么会走到[3]这里,是因为unused_arena_objects == NULL了,而且最后还设置了unused_arena_objects,这样系统中又有了"未使用"的arena了,接下来python就在[4]处对一个arena进行初始化了。

17.2.4 内存池

可用pool缓冲池--usedpools

通过#define SMALL_REQUEST_THRESHOLD 512我们知道python内部默认的小块内存与大块内存的分界点定在512个字节。也就是说,当申请的内存小于512个字节,pymalloc_alloc会在内存池中申请内存,而当申请的内存超过了512字节,那么pymalloc_alloc将退化为malloc,通过操作系统来申请内存。当然,通过修改python源代码我们可以改变这个值,从而改变python的默认内存管理行为。

当申请的内存小于512字节时,python会使用area所维护的内存空间。那么python内部对于area的个数是否有限制呢?换句话说,python对于这个小块空间内存池的大小是否有限制?其实这个决策取决于用户,python提供了一个编译符号,用于控制是否限制内存池的大小,不过这里不是重点,只需要知道就行。

尽管我们在前面花了不少篇幅介绍arena,同时也看到arena是python的小块内存池的最上层结构,所有arena的集合实际就是小块内存池。然而在实际的使用中,python并不直接与arenas和arena数组打交道。当python申请内存时,最基本的操作单元并不是arena,而是pool。估计到这里懵了,别急,慢慢来。

举个例子,当我们申请一个28字节的内存时,python内部会在内存池寻找一块能够满足需求的pool,从中取出一个block返回,而不会去寻找arena。这实际上是由pool和arena的属性决定的,在python中,pool是一个有size概念的内存管理抽象体,一个pool中的block总是有确定的大小,这个pool总是和某个size class index对应,还记得pool_header中的那个szidx么?而arena是没有size概念的内存管理抽象体。这就意味着,同一个arena在某个时刻,其内部的pool集合可能都是32字节的block;而到了另一个时刻,由于系统需要,这个arena可能被重新划分,其中的pool集合可能改为64字节的block了,甚至pool集合中一般的pool管理32字节,另一半管理64字节。这就决定了在进行内存分配和销毁时,所有的动作都是在pool上完成的。

当然内存池中的pool不仅仅是一个有size概念的内存管理抽象体,更进一步的,它还是一个有状态的内存管理抽象体。一个pool在python运行的任何一个时刻,总是处于一下三种状态中的一种:

used状态:pool中至少有一个block已经被使用,并且至少有一个block未被使用。这种状态的pool受控于python内部维护的usedpools数组。

full状态:pool中所有的block都已经被使用,这种状态的pool在arena中,但是不再arena的freepools链表中。

empty状态:pool中所有的block都未被使用,处于这个状态的pool的集合通过其pool_header中的nextpool构成一个链表,这个链表的表头就是arena中的freepools。

请注意:arena中处于full状态的pool是各自独立,没有像其他状态的pool一样,连接成一个链表。

我们从图中看到所有的处于used状态的pool都被置于usedpools的控制之下。python内部维护的usedpools数组是一个非常巧妙的实现,维护着所有的处于used状态的pool。当申请内存时,python就会通过usedpools寻找到一个可用的pool(处于used状态),从中分配一个block。因此我们想,一定有一个usedpools相关联的机制,完成从申请的内存的大小到size class index之间的转换,否则python就无法找到最合适的pool了。这种机制和usedpools的结构有着密切的关系,我们看一下它的结构。

//obmalloc.c
typedef uint8_t block;
#define PTA(x)  ((poolp )((uint8_t *)&(usedpools[2*(x)]) - 2*sizeof(block *)))
#define PT(x)   PTA(x), PTA(x)

//NB_SMALL_SIZE_CLASSES之前好像出现过,但是不用说也知道这表示当前配置下有多少个不同size的块
//在我当前的机器就是512/8=64个,对应的size class index就是从0到63
#define NB_SMALL_SIZE_CLASSES   (SMALL_REQUEST_THRESHOLD / ALIGNMENT)

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)
#if NB_SMALL_SIZE_CLASSES > 8
    , PT(8), PT(9), PT(10), PT(11), PT(12), PT(13), PT(14), PT(15)
#if NB_SMALL_SIZE_CLASSES > 16
    , PT(16), PT(17), PT(18), PT(19), PT(20), PT(21), PT(22), PT(23)
#if NB_SMALL_SIZE_CLASSES > 24
    , PT(24), PT(25), PT(26), PT(27), PT(28), PT(29), PT(30), PT(31)
#if NB_SMALL_SIZE_CLASSES > 32
    , PT(32), PT(33), PT(34), PT(35), PT(36), PT(37), PT(38), PT(39)
#if NB_SMALL_SIZE_CLASSES > 40
    , PT(40), PT(41), PT(42), PT(43), PT(44), PT(45), PT(46), PT(47)
#if NB_SMALL_SIZE_CLASSES > 48
    , PT(48), PT(49), PT(50), PT(51), PT(52), PT(53), PT(54), PT(55)
#if NB_SMALL_SIZE_CLASSES > 56
    , PT(56), PT(57), PT(58), PT(59), PT(60), PT(61), PT(62), PT(63)
#if NB_SMALL_SIZE_CLASSES > 64
#error "NB_SMALL_SIZE_CLASSES should be less than 64"
#endif /* NB_SMALL_SIZE_CLASSES > 64 */
#endif /* NB_SMALL_SIZE_CLASSES > 56 */
#endif /* NB_SMALL_SIZE_CLASSES > 48 */
#endif /* NB_SMALL_SIZE_CLASSES > 40 */
#endif /* NB_SMALL_SIZE_CLASSES > 32 */
#endif /* NB_SMALL_SIZE_CLASSES > 24 */
#endif /* NB_SMALL_SIZE_CLASSES > 16 */
#endif /* NB_SMALL_SIZE_CLASSES >  8 */
};

感觉这个数组有点怪异,别急我们来画图看一看

考虑一下当申请28字节的情形,前面我们说到,python首先会获得size class index,显然这里是3。那么在usedpools中,寻找第3+3=6个元素,发现usedpools[6]的值是指向usedpools[4]的地址。好晕啊,好吧,现在对照pool_header的定义来看一看usedpools[6] -> nextpool这个指针指向哪里了呢?

//obmalloc.c
/* Pool for small blocks. */
struct pool_header {
    union { block *_padding;
            uint count; } ref;          /* 当然pool里面的block数量    */
    block *freeblock;                   /* 一个链表,指向下一个可用的block   */
    struct pool_header *nextpool;       /* 指向下一个pool  */
    struct pool_header *prevpool;       /* 指向上一个pool       ""        */
    uint arenaindex;                    /* 在area里面的索引 */
    uint szidx;                         /* block的大小(固定值?后面说)     */
    uint nextoffset;                    /* 下一个可用block的内存偏移量         */
    uint maxnextoffset;                 /* 最后一个block距离开始位置的距离     */
};

显然是从usedpools[6](即usedpools+4)开始向后偏移8个字节(一个ref的大小加上一个freeblock的大小)后的内存,正好是usedpools[6]的地址(即usedpools+6),这是python内部的trick

想象一下,当我们手中有一个size class为32字节的pool,想要将其放入这个usedpools中时,要怎么做呢?从上面的描述我们知道,只需要进行usedpools[i+i] -> nextpool = pool即可,其中i为size class index,对应于32字节,这个i为3.当下次需要访问size class 为32字节(size class index为3)的pool时,只需要简单地访问usedpools[3+3]就可以得到了。python正是使用这个usedpools快速地从众多的pool中快速地寻找到一个最适合当前内存需求的pool,从中分配一块block。

//obmalloc.c
static int
pymalloc_alloc(void *ctx, void **ptr_p, size_t nbytes)
{
    block *bp;
    poolp pool;
    poolp next;
    uint size;
    ...
    LOCK();
    //获得size class index
    size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;
    //直接通过usedpools[size+size],这里的size不就是我们上面说的i吗?
    pool = usedpools[size + size];
    //如果usedpools中有可用的pool
    if (pool != pool->nextpool) {
        ... //有可用pool
    }
    ... //无可用pool,尝试获取empty状态的pool
}        

Pool的初始化

当python启动之后,在usedpools这个小块空间的内存池中,并不存在任何可用的内存,准确的说,不存在任何可用的pool。在这里,python采用了延迟分配的策略,即当我们确实开始申请小块内存的时候,python才建立这个内存池。正如之前提到的,当我们开始申请28字节的内存时,python实际将申请32字节的内存,然后会首先根据32字节对应的class size index(3)在usedpools中对应的位置查找,如果发现在对应的位置后面没有连接任何可用的pool,python会从"可用"arena链表中的第一个可用的arena中获取的一个pool。不过需要注意的是,当前获得的arena中包含的这些pools中可能会具有不同的class size index。

想象一下,当申请32字节的内存时,从"可用"arena中取出一个pool用作32字节的pool。当下一次内存分配请求分配64字节的内存时,python可以直接使用当前"可用"的arena的另一个pool即可,正如我们之前说的arena没有size class的属性,而pool才有。

//obmalloc.c
static int
pymalloc_alloc(void *ctx, void **ptr_p, size_t nbytes)
{
    block *bp;
    poolp pool;
    poolp next;
    uint size;
    ...
    LOCK();
    size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;
    pool = usedpools[size + size];
    //如果usedpools中有可用的pool
    if (pool != pool->nextpool) {
        ... //有可用pool
    }
    //无可用pool,尝试获取empty状态的pool
    if (usable_arenas == NULL) {
        //尝试申请新的arena,并放入"可用"arena链表
        usable_arenas = new_arena();
        if (usable_arenas == NULL) {
            goto failed;
        }
        usable_arenas->nextarena =
            usable_arenas->prevarena = NULL;
    }
    assert(usable_arenas->address != 0);

    //从可用arena链表中第一个arena的freepools中抽取一个可用的pool
    pool = usable_arenas->freepools;
    if (pool != NULL) {
        /* Unlink from cached pools. */
        usable_arenas->freepools = pool->nextpool;
        //调整可用arena链表中第一个arena中的可用pool的数量
        --usable_arenas->nfreepools;
        //如果调整之后变为0,则将该arena从可用arena链表中移除
        if (usable_arenas->nfreepools == 0) {
            /* Wholly allocated:  remove. */
            assert(usable_arenas->freepools == NULL);
            assert(usable_arenas->nextarena == NULL ||
                   usable_arenas->nextarena->prevarena ==
                   usable_arenas);

            usable_arenas = usable_arenas->nextarena;
            if (usable_arenas != NULL) {
                usable_arenas->prevarena = NULL;
                assert(usable_arenas->address != 0);
            }
        }
        else {
            /* nfreepools > 0:  it must be that freepools
             * isn\'t NULL, or that we haven\'t yet carved
             * off all the arena\'s pools for the first
             * time.
             */
            assert(usable_arenas->freepools != NULL ||
                   usable_arenas->pool_address <=
                   (block*)usable_arenas->address +
                       ARENA_SIZE - POOL_SIZE);
        }

    init_pool:
    	...
} 

可以看到,如果开始时"可用"arena链表为空,那么python会通过new_arena申请一个arena,开始构建"可用"arena链表。还记得我们之前遗留了一个问题吗?答案就在这里。在这里,一个脱离了"未使用"arena链表并转变为"可用"的arena被纳入了"可用"arena链表的控制。所以python会尝试从"可用"arena链表中的第一个arena所维护的pool集合中取出一个可用的pool。如果成功地取出了这个pool,那么python就会进行一些维护信息的更新工作,甚至在当前arena中可用的pool已经用完了之后,将该arena从"可用"arena链表中移除

好了,现在我们手里有了一块用于32字节内存分配的pool,为了提高以后内存分配的效率,我们需要将这个pool放入到usedpools中。这一步就是我们上面代码中没贴的init

//obmalloc.c
static int
pymalloc_alloc(void *ctx, void **ptr_p, size_t nbytes)
{
	init_pool:
    	//将pool放入usedpools中
        next = usedpools[size + size]; /* == prev */
        pool->nextpool = next;
        pool->prevpool = next;
        next->nextpool = pool;
        next->prevpool = pool;
        pool->ref.count = 1;
    	//pool在之前就具有正确的size结构,直接返回pool中的一个block
        if (pool->szidx == size) {
            bp = pool->freeblock;
            assert(bp != NULL);
            pool->freeblock = *(block **)bp;
            goto success;
        }
        //	pool之前就具有正确的size结果,直接返回pool中的一个block
        pool->szidx = size;
        size = INDEX2SIZE(size);
        bp = (block *)pool + POOL_OVERHEAD;
        pool->nextoffset = POOL_OVERHEAD + (size << 1);
        pool->maxnextoffset = POOL_SIZE - size;
        pool->freeblock = bp + size;
        *(block **)(pool->freeblock) = NULL;
        goto success;
    }
}

具体的细节可以自己观察源代码去研究,这里不再写了,有点累

block的释放

考察完了对block的分配,是时候来看看对block的释放了。对block的释放实际上就是将一块block归还给pool,我们已经知道pool可能存在3种状态,在分别处于三种状态,它们的位置是各不相同的。

当我们释放一个block之后,可能会引起pool状态的转变,这种转变可分为两种情况

  • used状态转变为empty状态
  • full状态转变为used状态
//obmalloc.c
static int
pymalloc_free(void *ctx, void *p)
{
    poolp pool;
    block *lastfree;
    poolp next, prev;
    uint size;
    pool = POOL_ADDR(p);
    if (!address_in_range(p, pool)) {
        return 0;
    }

    LOCK();
    assert(pool->ref.count > 0);            /* else it was empty */
    //设置离散自由的block链表
    *(block **)p = lastfree = pool->freeblock;
    pool->freeblock = (block *)p;
    //如果!lastfree成立,那么意味着不存在lastfree,说明这个pool在释放block之前是满的
    if (!lastfree) {
        /* Pool was full, so doesn\'t currently live in any list:
         * link it to the front of the appropriate usedpools[] list.
         * This mimics LRU pool usage for new allocations and
         * targets optimal filling when several pools contain
         * blocks of the same size class.
         */
        //当前pool处于full状态,在释放一块block之后,需要将其转换为used状态
        //并重新链入到usedpools的头部
        --pool->ref.count;
        assert(pool->ref.count > 0);            /* else the pool is empty */
        size = pool->szidx;
        next = usedpools[size + size];
        prev = next->prevpool;
        pool->nextpool = next;
        pool->prevpool = prev;
        next->prevpool = pool;
        prev->nextpool = pool;
        goto success;
    }

    struct arena_object* ao;
    uint nf;  /* ao->nfreepools */
	
    //否则到这一步表示lastfree有效
    //pool回收了一个block之后,不需要从used状态转换为empty状态
    if (--pool->ref.count != 0) {
        /* pool isn\'t empty:  leave it in usedpools */
        goto success;
    }
    /* 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).
     */
    //否则说明pool为空
    next = pool->nextpool;
    prev = pool->prevpool;
    next->prevpool = prev;
    prev->nextpool = next;
	
    //将pool放入freepools维护的链表中
    ao = &arenas[pool->arenaindex];
    pool->nextpool = ao->freepools;
    ao->freepools = pool;
    nf = ++ao->nfreepools;

    if (nf == ao->ntotalpools) {
        //调整usable_arenas链表
        if (ao->prevarena == NULL) {
            usable_arenas = ao->nextarena;
            assert(usable_arenas == NULL ||
                   usable_arenas->address != 0);
        }
        else {
            assert(ao->prevarena->nextarena == ao);
            ao->prevarena->nextarena =
                ao->nextarena;
        }
        /* Fix the pointer in the nextarena. */
        if (ao->nextarena != NULL) {
            assert(ao->nextarena->prevarena == ao);
            ao->nextarena->prevarena =
                ao->prevarena;
        }
        //调整"未使用"arena链表
        ao->nextarena = unused_arena_objects;
        unused_arena_objects = ao;

        //程序走到这一步,表示是pool原先是used,释放block之后依旧是used
        //那么会将内存归还给操作系统
        _PyObject_Arena.free(_PyObject_Arena.ctx,
                             (void *)ao->address, ARENA_SIZE);
        //设置address,将arena的状态转为"未使用"
        ao->address = 0;                        /* mark unassociated */
        --narenas_currently_allocated;

        goto success;
    }
}

实际上在python2.4之前,python的arena是不会释放pool的。这样的话就会引起内存泄漏,比如我们申请10 * 1024 * 1024个16字节的小内存,这就意味着必须使用160MB的内存,由于python会默认全部使用arena(这一点我们没有提)来满足你的需求。但是当我们将所有16字节的内存全部释放了,这些内存也会回到arena的控制之中,这都没有问题。但是问题来了,这些内存是被arena控制的,并没有交给操作系统啊,,所以这160MB的内存始终会被python占用,如果后面程序再也不需要160MB如此巨大的内存,那么不就浪费了吗?

由于这种情况必须在大量持续申请小内存对象时才会出现,因为大的话会自动交给操作系统了,小的才会由arena控制,而持续申请大量小内存的情况几乎不会碰到,所以这个问题也就留在了 Python中。但是因为有些人发现了这个问题,所以这个问题在python2.5的时候就得到了解决。

因为早期的python,arena是没有区分"未使用"和"可用"两种状态的,到了python2.5中,arena已经可以将自己维护的pool集合释放,交给操作系统了,从而将"可用"状态转化为"未使用"状态。而当python处理完pool,就开始处理arena了。

而对arena的处理实际上分为了4中情况

  • 1.如果arena中所有的pool都是empty的,释放pool集合所占用的内存
  • 2.如果之前arena中没有了empty的pool,那么在"可用"链表中就找不到该arena,由于现在arena中有了一个pool,所以需要将这个arena链入到"可用"链表的表头
  • 3.如果arena中的empty的pool的个数为n,那么会从"可用"arena链表中开始寻找arena可以插入的位置,将arena插入到"可用"链表。这样操作的原因就在于"可用"arena链表实际上是一个有序的链表,从表头开始往后,每一个arena中empty的pool的个数,即nfreepools,都不能大于前面的arena,也不能小于后面的arena。保持这样有序性的原则是分配block时,是从"可用"链表的表头开始寻找可用arena的,这样就能保证如果一个arena的empty pool数量越多,它被使用的机会就越少。因此它最终释放其维护的pool集合的内存的机会就越大,这样就能保证多余的内存会被归还给操作系统
  • 4.其他情况,则不对arena进行任何处理。

内存池全景

前面我们已经提到了,对于一个用c开发的庞大的软件(python是一门高级语言,但是执行对应代码的解释器则可以看成是c的一个软件),其中的内存管理可谓是最复杂、最繁琐的地方了。不同尺度的内存会有不同的抽象,这些抽象在各种情况下会组成各式各样的链表,非常复杂。但是我们还是有可能从一个整体的尺度上把握整个内存池,尽管不同的链表变幻无常,但我们只需记住,所有的内存都在arenas(或者说那个存放多个arena的数组)的掌握之中 。

17.3 循环引用之垃圾回收

17.3.1 引用计数之垃圾回收

现在绝大部分语言都实现了垃圾回收机制,也包括python。然而python的垃圾回收和java,c#等语言有一个很大的不同,那就是python中大多数对象的生命周期是通过对象的引用计数来管理的,这一点在开始的章节我们就说了,对于python中最基础的对象PyObject,有两个属性,一个是该对象的类型,还有一个就是引用计数(ob_refcnt)。不过从广义上将,引用计数也算是一种垃圾回收机制,而且它是一中最简单最直观的垃圾回收计数。尽管需要一个值来维护引用计数,但是引用计数有一个最大的优点:实时性。任何内存,一旦没有指向它的引用,那么就会被回收。而其他的垃圾回收技术必须在某种特定条件下(比如内存分配失败)才能进行无效内存的回收。

引用计数机制所带来的维护引用计数的额外操作,与python运行中所进行的内存分配、释放、引用赋值的次数是成正比的。这一点,相对于主流的垃圾回收技术,比如标记--清除(mark--sweep)、停止--复制(stop--copy)等方法相比是一个弱点,因为它们带来额外操作只和内存数量有关,至于多少人引用了这块内存则不关心。因此为了与引用计数搭配、在内存的分配和释放上获得最高的效率,python设计了大量的内存池机制,比如小整数对象池、字符串的intern机制,列表的freelist缓冲池等等,这些大量使用的面向特定对象的内存池机制正是为了弥补引用计数的软肋。

其实对于现在的cpu和内存来说,上面的问题都不是什么问题。但是引用计数还存在一个致命的缺陷,这一缺陷几乎将引用计数机制在垃圾回收技术中判处了"死刑",这一技术就是"循环引用"。而且也正是因为"循环引用"这个致命伤,导致在狭义上并不把引用计数机制看成是垃圾回收技术

在介绍循环引用之前,先来看看python引用计数什么时候会增加,什么时候会减少。

引用计数加一

  • 对象被创建:a=1
  • 对象被引用:b=a
  • 对象被作为参数传到一个函数中,func(a)
  • 对象作为列表、元组等其他容器里面的元素

引用计数减一

  • 对象别名被显式的销毁:del a
  • 对象的引用指向了其他的对象:a=2
  • 对象离开了它的作用域,比如函数的局部变量,在函数执行完毕的时候,也会被销毁(如果没有获取栈帧的话),而全局变量则不会
  • 对象所在的容器被销毁,或者从容器中删除等等

查看引用计数

查看一个对象的引用计数,可以通过sys.getrefcount(obj),但是由于作为getrefcount这个函数的参数,所以引用计数会多1。

我们之前说,a = "mashiro",相当于把a和a对应的值组合起来放在了命名空间里面,那么你认为这个a对应的值是什么呢?难道是"mashiro"这个字符串吗?其实从python的层面上来看的话确实是这样,但是在python的底层,其实存储的是字符数组"mashiro"对应地址,我总觉得前面章节好像说错了。

b=a在底层中则表示把a的指针拷贝给了b,是的你没有看错,都说python传递的是符号,但是在底层就是传递了一个指针,无论什么传递的都是指针,在python的层面上传递就是符号、或者就是引用。所以我们看到, 每当多了一个引用,那么"mashiro"(在c的层面上是一个结构体,PyUnicodeObject)的引用计数就会加1.

而每当减少一个引用,引用计数就会减少1。尽管我们用sys.getrefcount得到的结果是2,但是当这个函数执行完,由于局部变量的销毁,其实结果已经变成了1。因此引用计数很方便,就是当一片空间没有人引用了,那么就直接销毁。尽管维护这个引用计数需要消耗资源,可还是那句话,对于如今的硬件资源来说,是完全可以接受的,毕竟引用计数真的很方便。但是,是的我要说但是了,就是我们之前的那个循环引用的问题。

l1 = []
l2 = []

l1.append(l2)
l2.append(l1)

del l1, l2

初始的时候,l1和l2指向的内存的引用计数都为1,但是l1.append(l2),那么l2指向内存的引用计数变成了2,同理l2.append(l1)导致l1指向内存的引用计数也变成了2。因此当我们del l1, l2的时候,引用计数会从2变成1,因此l1和l2都不会被回收,因为我们是希望回收l1和l2的,但是如果只有引用计数的话,那么显然这两者是回收不了的。因此这算是引用计数的最大的缺陷,因为会导致内存泄漏。因此python为了解决这个问题,就必须在引用计数机制之上又引入了新的主流垃圾回收计数:标记--清除和分代收集计数来弥补这个最致命的漏洞。

17.3.2 三色标记模型

无论何种垃圾回收机制,一般都分为两个阶段:垃圾检测和垃圾回收。垃圾检测是从所有的已经分配的内存中区别出"可回收"和"不可回收"的内存,而垃圾回收则是使操作系统重新掌握垃圾检测阶段所标识出来的"可回收"内存块。所以垃圾回收,并不是说直接把这块内存的数据清空了,而是说将使用权从新交给了操作系统,不会自己霸占了。下面我们来看看标记--清除(mark--sweep)方法是如何实现的,并为这个过程建立一个三色标记模型,python中的垃圾回收正是基于这个模型完成的。

从具体的实现上来讲,标记--清除方法同样遵循垃圾回收的两个阶段,其简要过程如下:

  • 寻找根对象(root object)的集合,所谓的root object就是一些全局引用和函数栈的引用。这些引用所用的对象是不可被删除的,而这个root object集合也是垃圾检测动作的起点
  • 从root object集合出发,沿着root object集合中的每一个引用,如果能到达某个对象A,则称A是可达的(reachable),可达的对象也不可被删除。这个阶段就是垃圾检测阶段
  • 当垃圾检测阶段结束后,所有的对象分为了可达的(reachable)和不可达的(unreachable)。而所有可达对象都必须予以保留,而不可达对象所占用的内存将被回收。

在垃圾回收动作被激活之前,系统中所分配的所有对象和对象之间的引用组成了一张有向图,其中对象是图中的节点,而对象间的引用则是图的边。我们在这个有向图的基础之上建立一个三个标注模型,更形象的展示垃圾回收的整个动作。当垃圾回收开始时,我们假设系统中的所有对象都是不可达的,对应在有向图上就是白色 。随后从垃圾回收的动作开始,沿着始于root object集合中的某个object的引用链,在某个时刻到达了对象A,那我们把A标记为灰色,灰色表示一个对象是可达的,但是其包含的引用还没有被检查。当我们检查了对象A所包含的所有引用之后,A将被标记为黑色,表示其包含的所有引用已经被检查过了。显然,此时A中引用的对象则被标记成了灰色。假如我们从root object集合开始,按照广度优先的策略进行搜索的话,那么不难想象,灰色节点对象集合就如同波纹一样,不断向外扩散,随着所有的灰色节点都变成了黑色节点,也就意味着垃圾检测阶段结束了。

17.4 python中的垃圾回收

如之前所说,python中主要的内存管理手段是引用计数机制,而标记--清除和分代收集只是为了打破循环引用而引入的补充技术。这一事实意味着python中的垃圾回收只关注可能会产生循环引用的对象,而像PyLongObject、PyUnicodeObject这些对象是绝对不可能产生循环引用的,因为它们内部不可能持有对其他对象的引用,所以这些直接通过引用计数机制就可以实现,而且后面我们说的垃圾回收也专指那些可能产生循环引用的对象。python中的循环引用只会总是发生在container对象之间,所谓container对象就是内部可持有对其他对象的引用的对象,比如list、dict、class、instance等等。当python开始垃圾回收机制开始运行时,只需要检查这些container对象,而对于PyLongObject、PyUnicodeObject则不需要理会,这使得垃圾回收带来的开销只依赖于container对象的数量,而非所有对象的数量。为了达到这一点,python就必须跟踪所创建的每一个container对象,并将这些对象组织到一个集合中,只有这样,才能将垃圾回收的动作限制在这些对象上。而python采用了一个双向链表,所有的container对象在创建之后,都会被插入到这个链表当中。

17.4.1 可收集对象链表

在对python对象机制的分析当中我们已经看到,任何一个python对象都可以分为两部分,一部分是PyObject_HEAD,另一部分是对象自身的数据。然而对于一个需要被垃圾回收机制跟踪的container来说,还不够,因为这个对象还必须链入到python内部的可收集对象链表中。而一个container对象要想成为一个可收集的对象,则必须加入额外的信息,这个信息位于PyObject_HEAD之前,称为PyGC_Head

//objimpl.h
typedef union _gc_head {
    struct {
        union _gc_head

以上是关于《python解释器源码剖析》第17章--python的内存管理与垃圾回收的主要内容,如果未能解决你的问题,请参考以下文章

《python解释器源码剖析》第13章--python虚拟机中的类机制

《python解释器源码剖析》第10章--python虚拟机中的一般表达式

《python解释器源码剖析》第11章--python虚拟机中的控制流

《python解释器源码剖析》第16章--python的多线程机制

《python解释器源码剖析》第15章--python模块的动态加载机制

《python解释器源码剖析》第3章--python中的字符串对象