码迷,mamicode.com
首页 > 编程语言 > 详细

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

时间:2020-01-16 19:03:46      阅读:89      评论:0      收藏:0      [点我收藏+]

标签:resize   ++   new   共存   状态   如何   需要   检测   合成   

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 *gc_next;
        union _gc_head *gc_prev;
        Py_ssize_t gc_refs;
    } gc;
    long double dummy;  /* force worst-case alignment */
    // malloc returns memory block aligned for any built-in types and
    // long double is the largest standard C type.
    // On amd64 linux, long double requires 16 byte alignment.
    // See bpo-27987 for more discussion.
} PyGC_Head;

所以,对于python所创建的可收集container对象,其内存分布与我们之前所了解的内存布局是不同的,我们可以从可收集container对象的创建过程中窥见其内存分布。

//Modules/gcmodule.c
PyObject *
_PyObject_GC_New(PyTypeObject *tp)
{
    PyObject *op = _PyObject_GC_Malloc(_PyObject_SIZE(tp));
    if (op != NULL)
        op = PyObject_INIT(op, tp);
    return op;
}

PyObject *
_PyObject_GC_Malloc(size_t basicsize)
{
    return _PyObject_GC_Alloc(0, basicsize);
}

#define GC_UNTRACKED                    _PyGC_REFS_UNTRACKED
#define _PyGC_REFS_UNTRACKED                    (-2) //该行位于objimpl.h中

static PyObject *
_PyObject_GC_Alloc(int use_calloc, size_t basicsize)
{
    PyObject *op;
    PyGC_Head *g;
    size_t size;
    //将对象和PyGC_Head所需内存加起来
    if (basicsize > PY_SSIZE_T_MAX - sizeof(PyGC_Head))
        return PyErr_NoMemory();
    size = sizeof(PyGC_Head) + basicsize;
    //为对象本身和PyGC_Head申请内存
    if (use_calloc)
        g = (PyGC_Head *)PyObject_Calloc(1, size);
    else
        g = (PyGC_Head *)PyObject_Malloc(size);
    if (g == NULL)
        return PyErr_NoMemory();
    g->gc.gc_refs = 0;
    _PyGCHead_SET_REFS(g, GC_UNTRACKED);
    _PyRuntime.gc.generations[0].count++; /* number of allocated GC objects */
    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();
        _PyRuntime.gc.collecting = 0;
    }
    op = FROM_GC(g);
    return op;
}

因此我们可以很清晰的看到,当python为可收集的container对象申请内存空间时,为PyGC_Head也申请了空间,并且其位置位于container对象之前。所以对于PyListObject、PyDictObject等container对象的内存分布的推测就应该变成这样。

技术图片

在可收集container对象的内存分布中,内存分为三个部分,首先第一块用于垃圾回收机制,然后紧跟着的是python中所有对象都会有的PyObject_HEAD,最后才是container自身的数据。这里的container对象,既可以是PyDictObject、也可以是PyListObject等等。

//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 */
    // malloc returns memory block aligned for any built-in types and
    // long double is the largest standard C type.
    // On amd64 linux, long double requires 16 byte alignment.
    // See bpo-27987 for more discussion.
} PyGC_Head;

再来看看PyGC_Head的模样,里面除了两个建立链表结构的前向和后向指针外,还有一个gc_ref,而这个值被初始化为GC_UNTRACKED,在上面的代码中可以看到。这个变量对于垃圾回收的运行至关重要,但是在分析它之前我们还需要了解一些其他的东西。

当垃圾回收机制运行期间,我们需要在一个可收集的container对象的PyGC_Head部分和PyObject_HEAD部分之间来回切换。更清楚的说,某些时候,我们持有一个对象A的PyObject_HEAD的地址,但是我们需要根据这个地址来获得PyGC_Head的地址;而且某些时候,我们又需要反过来进行逆运算。而python提供了两个地址之间的转换算法

//gcmodule.c
//AS_GC,根据PyObject_HEAD得到PyGC_Head
#define AS_GC(o) ((PyGC_Head *)(o)-1)
//FROM_GC,从PyGC_Head那里得到PyObject_HEAD
#define FROM_GC(g) ((PyObject *)(((PyGC_Head *)g)+1))

//objimpl.h
#define _Py_AS_GC(o) ((PyGC_Head *)(o)-1)

在PyGC_Head中,出现了用于建立链表的两个指针,只有将创建的可收集container对象链接到python内部维护的可收集对象链表中,python的垃圾回收机制才能跟踪和处理这个container对象。但是我们发现,在创建可收集container对象之时,并没有立刻将这个对象链入到链表中。实际上,这个动作是发生在创建某个container对象最后一步,以PyListObject的创建举例。

//listobject.c
PyObject *
PyList_New(Py_ssize_t size)
{
    PyListObject *op;
    ...
    Py_SIZE(op) = size;
    op->allocated = size;
    //创建PyListObject对象、并设置完属性之后,返回之前,通过这一步_PyObject_GC_TRACK将所创建的container对象链接到了python中的可收集对象链表中。
    _PyObject_GC_TRACK(op);
    return (PyObject *) op;
}

//objimpl.h
#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);

前面我们说过,python会将自己的垃圾回收机制限制在其维护的可收集对象链表上,因为所有的循环引用一定是发生这个链表的一群对象之间。在_PyObject_GC_TRACK之后,我们创建的container对象也就置身于python垃圾回收机制的掌控机制当中了。

同样的,python还提供将一个container对象从链表中摘除的方法,显然这个方法应该会在对象被销毁的时候调用。

//objimpl.h
#define _PyObject_GC_UNTRACK(o) do {     PyGC_Head *g = _Py_AS_GC(o);     assert(_PyGCHead_REFS(g) != _PyGC_REFS_UNTRACKED);     _PyGCHead_SET_REFS(g, _PyGC_REFS_UNTRACKED);     g->gc.gc_prev->gc.gc_next = g->gc.gc_next;     g->gc.gc_next->gc.gc_prev = g->gc.gc_prev;     g->gc.gc_next = NULL;     } while (0);

很明显,_PyObject_GC_UNTRACK只是_PyObject_GC_TRACK的逆运算而已

技术图片

17.4.2 分代的垃圾收集

无论什么语言,写出来的程序都有共同之处。那就是不同对象的声明周期会存在不同,有的对象所占的内存块的生命周期很短,而有的内存块的生命周期则很长,甚至可能从程序的开始持续到程序结束。这两者的比例大概在80~90%

这对于垃圾回收机制有着重要的意义,因为我们已经知道,像标记--清除这样的垃圾回收机制所带来的额外操作实际上是和系统中内存块的数量是相关的,当需要回收的内存块越多的时候,垃圾检测带来的额外操作就越多,相反则越少。因此我们可以采用一种空间换时间的策略,因为目前所有对象都在一个链子上,每当进行垃圾回收机制的时候,都要把所有对象都检查一遍。而其实也有不少比较稳定的对象(在多次垃圾回收的洗礼下能活下来),我们完全没有必要每次都检查,或者说检查的频率可以降低一些。于是聪明如你已经猜到了,我们再来一根链子不就可以了,把那些认为比较稳定的对象移到另外一条链子上,而新的链子进行垃圾回收的频率会低一些,总之频率不会像初始的链子那么高。

所以这种思想就是:将系统中的所有内存块根据其存活时间划分为不同的集合,每一个集合就成为一个"代",垃圾回收的频率随着"代"的存活时间的增大而减小,也就是说,存活的越长的对象就越可能不是垃圾,就越可能是程序中需要一直存在的对象,就应该少去检测它。反正不是垃圾,你检了也白检。那么关键的问题来了,这个存活时间是如何被衡量的呢?或者我们说当对象比较稳定的时候的这个稳定是如何衡量的呢?没错,我们上面已经暴露了,就是通过经历了几次垃圾回收动作来评判,如果一个对象经历的垃圾回收次数越多,那么显然其存活时间就越长。因为python的垃圾回收器,每当条件满足时(至于什么条件我们后面会说),就会进行一次垃圾回收(注意:不同的代的垃圾回收的频率是不同的),而每次扫黄的时候你都不在,吭,每次垃圾回收的时候你都能活下来,这就说明你存活的时间更长,或者像我们上面说的更稳定,那么就不应该再把你放在这个链子上了,而是会移动到新的链子上。而在新的链子上,进行垃圾回收的频率会降低,因为既然稳定了,检测就不必那么频繁了,或者说新的链子上触发垃圾回收所需要的时间更长了。

"代"似乎是一个比较抽象的概念,但在python中,你就把"代"想象成多个对象组成集合,或者你把"代"想象成链表(或者链子)也可以,因为这些对象都串在链表上面。而属于同一"代"的内存块都被链接在同一个链表中。而在python中总共存在三条链表,说明python中所有的对象总共可以分为三代,分别零代、一代、二代。一个"代"就是一条我们上面提到的可收集对象链表。而在前面所介绍的链表的基础之上,为了支持分代机制,我们需要的仅仅是一个额外的表头而已。

//Include/internal/mem.h
struct gc_generation {
    PyGC_Head head;
    int threshold; /* collection threshold */
    int count; /* count of allocations or collections of younger
                  generations */
};
#define NUM_GENERATIONS 3

//gcmodule.c
#define GEN_HEAD(n) (&_PyRuntime.gc.generations[n].head)
    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},
    };
    state->generation0 = GEN_HEAD(0);

上面这个维护了三个gc_generation结构的数组,通过这个数组控制了三条可收集对象链表,这就是python中用于分代垃圾收集的三个"代"。

而我们在之前上面说的_PyObject_GC_TRACK中会看到_PyGC_generation0,它不偏不斜,指向的正是第0代链表。

对于每一个gc_generation,其中的count记录了当前这条可收集对象链表中一共有多少个对象。而在_PyObject_GC_Alloc中我们可以看到每当分配了内存,就会进行_PyRuntime.gc.generations[0].count++动作,将第0代链表中所维护的内存块数量加1,这预示着所有新创建的对象实际上都会被加入到0代链表当中,而这一点也确实如此,已经被_PyObject_GC_TRACK证明了。而且我们发现这里是先将数量加1,然后再将新的container对象(内存块)才会被链接到第0代链表当中,当然这个无所谓啦。

而gc_generation中的threshold则记录该条可收集对象链表中最多可以容纳多少个可收集对象,从python的实现代码中,我们知道第0代链表中最多可以容纳700个对象(只可能是container对象)。而一旦第0代链表中的container对象超过了700个这个阈值,那么会立刻除法垃圾回收机制。

static Py_ssize_t
collect_generations(void)
{
    int i;
    Py_ssize_t n = 0;
    for (i = NUM_GENERATIONS-1; i >= 0; i--) {
        //当count大于threshold的时候,但是这个仅仅针对于0代链表
        if (_PyRuntime.gc.generations[i].count > _PyRuntime.gc.generations[i].threshold) {
            if (i == NUM_GENERATIONS - 1
                && _PyRuntime.gc.long_lived_pending < _PyRuntime.gc.long_lived_total / 4)
                continue;
            n = collect_with_callback(i);
            break;
        }
    }
    return n;
}

这里面虽然写了一个for循环,但是只有当第0代链表的count超过了threshold的时候才会触发垃圾回收,那么1代链表和2代链表触发垃圾回收的条件又是什么呢?当0代链表触发了10次垃圾回收的时候,会触发一次1代链表的垃圾回收。当1代链表触发了10次垃圾回收的时候,会触发一次2代链表的垃圾回收。另外:

在清理1代链表的时候,会顺带清理0代链表

在清理2代链表的时候,会顺带清理0代链表和1代链表

17.4.3 python中的标记--清除

我们上面说到,当清理1代链表会顺带清理0代链表,总是就是把比自己"代"要小的链子也清理了。那么这是怎么做到的呢?其实答案就在gc_list_merge函数中,如果清理的是1代链表,那么在开始垃圾回收之前,python会将0代链表(比它年轻的),整个地链接到1代链表之后。

//gcmodule.c
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);
}

以我们举的例子来说的话,那么这里的from就是0代链表,to就是1代链表,所以此后的标记--清除算法就将在merge之后的那一条链表上进行。

在介绍python中的标记--清除垃圾回收方法之前,我们需要建立一个循环引用的最简单例子

list1 = []
list2 = []

list1.append(list2)
list2.append(list1)

# 注意这里多了一个外部引用
a = list1

list3 = []
list4 = []
list3.append(list4)
list4.append(list3)

技术图片

上面的数字指的是当前对象的引用计数ob_refcnt的值

17.4.3.1 寻找root object集合

为了使用标记--清除算法,按照我们之前对垃圾收集算法的一般性描述,首先我们需要找到root object,那么在我们上面的那幅图中,哪些是属于root object呢?

让我们换个角度来思考,前面提到,root object是不能被删除的对象。也就是说,在可收集对象链表的外部存在着某个引用在引用这个对象,删除这个对象会导致错误的行为,那么在我们当前这个例子中只有list1是属于root object的。但这仅仅是观察的结果,那么如何设计一种算法来得到这个结果呢?

我们注意到这样一个事实,如果两个对象的引用计数都为1,但是仅仅它们之间存在着循环引用,那么这两个对象是需要被回收的,也就是说,尽管它们的引用计数表现为非0,但是实际上有效的引用计数为0。这里,我们提出了有效引用计数的概念,为了从引用计数中获得优秀的引用计数,必须将循环引用的影响取出,也就是说,这个闭环从引用中摘除,而具体的实现就是两个对象各自的引用值都减去1。这样一来,两个对象的引用计数都成为了0,这样我们便挥去了循环引用的迷雾,是有效引用计数出现了真身。那么如何使两个对象的引用计数都减1呢,很简单,假设这两个对象为A和B,那么从A出发,由于它有一个对B的引用,则将B的引用计数减1;然后顺着引用达到B,发现它有一个对A的引用,那么同样会将A的引用减1,这样就完成了循环引用对象间环的删除。

总结一下就是,python会寻找那些具有循环引用的、但是没有被外部引用的对象,并尝试把它们的引用计数都减去1

但是这样就引出了一个问题,假设可收集对象链表中的container对象A有一个对对象C的引用,而C并不在这个链表中,如果将C的引用计数减去1,而最后A并没有被回收,那么显然,C的引用计数被错误地减少1,这将导致未来的某个时刻对C的引用会出现悬空。这就要求我们必须在A没有被删除的情况下回复C的引用计数,可是如果采用这样的方案的话,那么维护引用计数的复杂度将成倍增长。换一个角度,其实我们有更好的做法,我们不改动真实的引用计数,而是改动引用计数的副本。对于副本,我们无论做什么样的改动,都不会影响对象生命周期的维护,因为这个副本的唯一作用就是寻找root? object集合,而这个副本就是PyGC_Head中的gc.gc_ref。在垃圾回收的第一步,就是遍历可收集对象链表,将每个对象的gc.gc_ref的值设置为其ob_refcnt的值。

//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(_PyGCHead_REFS(gc) == GC_REACHABLE);
        _PyGCHead_SET_REFS(gc, Py_REFCNT(FROM_GC(gc)));
        assert(_PyGCHead_REFS(gc) != 0);
    }
}

//而接下来的动作就是要将环引用从引用中摘除
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操作中。比如:我们来看看PyListObject对象所定义traverse操作。

//object.h
typedef int (*visitproc)(PyObject *, void *);
typedef int (*traverseproc)(PyObject *, visitproc, void *);

//listobject.c
PyTypeObject PyList_Type = {
    ...
    (traverseproc)list_traverse,                /* tp_traverse */
    ...
};

static int
list_traverse(PyListObject *o, visitproc visit, void *arg)
{
    Py_ssize_t i;

    for (i = Py_SIZE(o); --i >= 0; )
        //对列表中的每一个元素都进行回调的操作
        Py_VISIT(o->ob_item[i]);
    return 0;
}

//gcmodule.c
/* A traversal callback for subtract_refs. */
static int
visit_decref(PyObject *op, void *data)
{
    assert(op != NULL);
    //PyObject_IS_GC判断op指向的对象是不是被垃圾收集监控的
    //标识container对象是被垃圾收集监控的
    if (PyObject_IS_GC(op)) {
        PyGC_Head *gc = AS_GC(op);
        assert(_PyGCHead_REFS(gc) != 0); /* else refcount was too small */
        if (_PyGCHead_REFS(gc) > 0)
            _PyGCHead_DECREF(gc);
    }
    return 0;
}

在完成了subtract_refs之后,可收集对象链表中所有container对象之间的环引用就被摘除了。这时有一些container对象的PyGC_Head.gc_ref还不为0,这就意味着存在对这些对象的外部引用,这些对象就是开始标记--清除算法的root object。

技术图片

技术图片

估计有人不明白引用计数是加在什么地方,其实变量=值在python中,变量得到的都是值的指针,a =?1,表示是在命名空间里面会有"a": 1这个键值对,但看似是这样,其实存储的并不是1,而是1这个结构体(python对象在底层是一个结构体)的指针,这个结构体存储在堆区。我们获取a的引用计数,其实是获取a指向的这个对象的引用计数,此时为1,如果b=a,在底层就等价于把a存储的内容(指针)拷贝给了b,那么此时a和b存储的指针指的都是同一个对象,那么这个对象的引用计数就变成了2。如果再来个b=2,那么表示再创建一个结构体存储的值为2,然后让b存储新的结构体的指针。那么原来的结构体的引用计数就从2又变成了1。

所以为什么初始的时候,list1的引用计数是3就很明显了,list1的引用计数指的其实是list1这个变量对应的值(或者说在底层,list1存储的指针指向的值)的引用计数,所以一旦创建一个变量那么引用计数会自动增加为1,然后a也指向了list1所指向的内存,并且list1又作为list2的一个元素(这个位置的元素存储了指向list1的指针),所以引用计数总共是3。

技术图片

由于sys.getrefcount函数本身会多一个引用,所以减去1的话,那么都是3。表示它们指向的内存存储的值的引用计数为3。sys.getrefcount(a) -> 4,这个时候a就想到了,除了我,还有两位老铁指向了我指向的内存。

17.4.3.2 垃圾标记

假设我们现在执行了删除操作del?list1, list2,?list3, list4,那么成功地寻找到root object集合之后,我们就可以从root object触发,沿着引用链,一个接一个地标记不能回收的内存,由于root object集合中的对象是不能回收的,因此,被这些对象直接或间接引用的对象也是不能回收的,比如这里的list2,即便del list2,但是因为list1不能回收,而又append了list2,所以list2指向的内存也是不可以释放的。下面在从root object出发前,我们首先需要将现在的内存链表一分为二,一条链表维护root object集合,成为root链表,而另一条链表中维护剩下的对象,成为unreachable链表。之所以要分解成两个链表,是出于这样一种考虑:显然,现在的unreachable链表是名不副实的,因为里面可能存在被root链表中的对象直接或者间接引用的对象,这些对象也是不可以回收的,因此一旦在标记中发现了这样的对象,那么就应该将其从unreachable中移到root链表中;当完成标记之后,unreachable链表中剩下的对象就是名副其实的垃圾对象了,那么接下来的垃圾回收只需要限制在unreachable链表中即可。

为此python专门准备了一条名为unreachable的链表,通过move_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;
        //[1]:如果是root object
        if (_PyGCHead_REFS(gc)) {
            PyObject *op = FROM_GC(gc);
            traverseproc traverse = Py_TYPE(op)->tp_traverse;
            assert(_PyGCHead_REFS(gc) > 0);
            //设置其gc_refs为GC_REACHABLE
            _PyGCHead_SET_REFS(gc, GC_REACHABLE);
            (void) traverse(op,
                            (visitproc)visit_reachable,
                            (void *)young);
            next = gc->gc.gc_next;
            if (PyTuple_CheckExact(op)) {
                _PyTuple_MaybeUntrack(op);
            }
        }
        else {
            //[2]:对于非root object,移到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);
        //[3]:对于还没有处理的对象,恢复其gc_refs
        if (gc_refs == 0) {
            _PyGCHead_SET_REFS(gc, 1);
        }
        //[4]:对于已经被挪到unreachable链表中的对象,将其再次挪动到原来的链表
        else if (gc_refs == GC_TENTATIVELY_UNREACHABLE) {
            gc_list_move(gc, reachable);
            _PyGCHead_SET_REFS(gc, 1);
        }

         else {
            assert(gc_refs > 0
                   || gc_refs == GC_REACHABLE
                   || gc_refs == GC_UNTRACKED);
         }
    }
    return 0;
}

在move_unreachable中,沿着可收集对象链表依次向前,并检查其PyGC_Head.gc.gc_ref值,我们发现这里的动作是遍历链表,而并非从root object集合出发,遍历引用链。这会导致一个微妙的结果,即当检查到一个gc_ref为0的对象时,我们并不能立即断定这个对象就是垃圾对象。因为在这个对象之后的对象链表上,也许还会遇到一个root object,而这个root object引用该对象。所以这个对象只是一个可能的垃圾对象,因此我们才要将其标志为GC_TENTATIVELY_UNREACHABLE,但是还是通过gc_list_move将其搬到了unreachable链表中,咦,难道不会出问题吗?别急,我们马上就会看到, python还留了后手。

当在move_unreachable中遇到一个gc_refs不为0的对象A时,显然,A是root object或者是从某个root object开始可以引用到的对象,而A所引用的所有对象也都是不可回收的对象。因此在代码的[1]处下面,我们看到会再次调用与特定对象相关的transverse操作,依次对A所引用的对象调用visit_reachable。在visit_reachable的[4]处我们发现,如果A所引用的对象之前曾被标注为GC_TENTATIVELY_UNREACHABLE,那么现在A可以访问到它,意味着它也是一个不可回收的对象,所以python会再次从unreachable链表中将其搬回到原来的链表。注意:这里的reachable,就是move_unreachable中的young,也就是我们所谓的root object链表。python还会将其gc_refs设置为1,表示该对象是一个不可回收对象。同样在[1]处,我们看到对A所引用的gc_refs为0的对象,其gc_refs也被设置成了1。想一想这是什么对象呢?显然它就是在链表move_unreachable操作中还没有访问到的对象,这样python就直接掐断了之后move_unreachable访问它时将其移动到unreachable链表的诱因。

当move_unreachable完成之后,最初的一条链表就被切分成了两条链表,在unreachable链表中,就是我们发现的垃圾对象,是垃圾回收的目标。但是等一等,在unreachable链表中,所有的对象都可以安全回收吗?其实,垃圾回收在清理对象的时候,默认是会清理的,但是一旦当我们定义了函数__del__,那么在清理对象的时候就会调用这个__del__方法,因此也叫析构函数,这是python为开发人员提供的在对象被销毁时进行某些资源释放的Hook机制。在python3中,即使我们重写了也没事,因为python会把含有__del__函数的PyInstanceObject对象都统统移动到一个名为garbage的PyListObject对象中。

17.4.4.4 垃圾回收

要回收unreachable链表中的垃圾对象,就必须先打破对象间的循环引用,前面我们已经阐述了如何打破循环引用的办法,下面来看看具体的销毁过程

//gcmodule.c
static int
gc_list_is_empty(PyGC_Head *list)
{
    return (list->gc.gc_next == list);
}

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

        if (_PyRuntime.gc.debug & DEBUG_SAVEALL) {
            PyList_Append(_PyRuntime.gc.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) {
            /* object is still alive, move it, it may die later */
            gc_list_move(gc, old);
            _PyGCHead_SET_REFS(gc, GC_REACHABLE);
        }
    }
}

其中会调用container对象的类型对象中的tp_clear操作,这个操作会调整container对象中引用的对象的引用计数值,从而打破完成循环的最终目标。还是以PyListObject为例:

//listobject.c
static int
_list_clear(PyListObject *a)
{
    Py_ssize_t i;
    PyObject **item = a->ob_item;
    if (item != NULL) {
        i = Py_SIZE(a);
        //将ob_size调整为0
        Py_SIZE(a) = 0;
        //ob_item是一个二级指针,本来指向一个数组的指针
        //现在指向为NULL
        a->ob_item = NULL;
        //容量也设置为0
        a->allocated = 0;
        while (--i >= 0) {
            //数组里面元素也全部减少引用计数
            Py_XDECREF(item[i]);
        }
        //释放数组
        PyMem_FREE(item);
    }
    return 0;
}

我们注意到,在delete_garbage中,有一些unreachable链表中的对象会被重新送回到reachable链表(即delete_garbage的old参数)中,这是由于进行clear动作时,如果成功进行,则通常一个对象会把自己从垃圾回收机制维护的链表中摘除(也就是这里的collectable链表)。由于某些原因,对象可能在clear动作时,没有成功完成必要的动作,从而没有将自己从collectable链表摘除,这表示对象认为自己还不能被销毁,所以python需要讲这种对象放回到reachable链表中。

我们在上面看到了list_clear,假设是调用了list3的list_clear,那么不好意思,这个是对list4做的处理。因为list3和list4存在循环引用,如果调用了list3的list_clear会减少list4的引用计数,由于这两位老铁都被删除了,还惺惺相惜赖在内存里面不走,所以将list4的引用计数减少1之后,只能归于湮灭了,然后会调用其list_dealloc,注意:这时候调用的是list4的list_dealloc。

//listobjct.c
static void
list_dealloc(PyListObject *op)
{
    Py_ssize_t i;
    //从可收集链表中移除
    PyObject_GC_UnTrack(op);
    Py_TRASHCAN_SAFE_BEGIN(op)
    if (op->ob_item != NULL) {
        //依次遍历,减少内部元素的引用计数
        i = Py_SIZE(op);
        while (--i >= 0) {
            Py_XDECREF(op->ob_item[i]);
        }
        //释放内存
        PyMem_FREE(op->ob_item);
    }
    //缓冲池机制
    if (numfree < PyList_MAXFREELIST && PyList_CheckExact(op))
        free_list[numfree++] = op;
    else
        Py_TYPE(op)->tp_free((PyObject *)op);
    Py_TRASHCAN_SAFE_END(op)
}

我们知道调用list3的list_clear,减少内部元素引用计数的时候,导致list4引用计数为0。而一旦list4的引用计数为0,那么是不是也要执行和list3一样的list_clear动作呢?然后会发现list3的引用计数也为0了,因此list3也会被销毁。循环引用,彼此共生,销毁之路,怎能独自前行?最终list3和list4都会执行内部的list_dealloc,释放内部元素,调整参数,当然还有所谓的缓冲池机制等等。总之如此一来,list3和list4就都被安全地回收了。

17.4.4.5 总结

虽然有很多对象挂在垃圾收集机制监控的链表上,但是很多时候是引用计数机制在维护这些对象,只有引用计数无能为力的循环引用,垃圾收集机制才会起到作用(这里没有把引用计数机制看成垃圾回收,当然如果别人问你python的垃圾回收机制的时候,你也可以把引用计数机制加上)。事实上,如果不是循环引用的话,那么垃圾回收是无能为力的,因为挂在垃圾回收机制上的对象都是引用计数不为0的,如果为0早被引用计数机制干掉了。而引用计数不为0的情况只有两种:一种是被程序使用的对象,二是循环引用中的对象。被程序使用的对象是不能被回收的,所以垃圾回收只能处理那些循环引用的对象。

所以python的垃圾回收就是:引用计数为主,分代回收为辅,两者结合使用,后者主要是为了弥补前者的缺点而存在的。

17.5 python中的gc模块

这个gc模块,底层就是gcmodule,我们说这些模块底层是用c写的,当python编译好值,就内嵌在解释器里面了。我们可以导入它,但是在python安装目录上看不到。

gc.enable():开启垃圾回收

这个函数表示开启垃圾回收机制,默认是自动开启的。

gc.disable():关闭垃圾回收

import gc


class A:
    pass


# 关掉gc
gc.disable()


while True:
    a1 = A()
    a2 = A()
    # 此时内部出现了循环引用
    a1.__dict__["attr"] = a2
    a2.__dict__["attr"] = a1

    # 由于循环引用,此时是del a1, a2,光靠引用计数是删不掉的
    # 需要垃圾回收,但是我们给关闭了
    del a1, a2

技术图片

无限循环,并且每次循环都会创建新的对象,最终导致内存无限增大。

import gc


class A:
    pass


# 关掉gc
gc.disable()


while True:
    a1 = A()
    a2 = A()

技术图片

这里即使我们关闭了gc,但是每一次循环都会指向一个新的对象,而之前的对象由于没有人指向了,那么引用计数为0,直接就被引用计数机制干掉了,内存会一直稳定,不会出现增长。所以我们看到,即使关闭了gc,但是对于那些引用计数为0的,该删除还是会删除的。所以引用计数很简单,就是按照对应的规则该加1加1,该减1减1,一旦为0直接销毁。而当出现循环引用的时候,才需要gc闪亮登场。这里关闭了gc,但是没有循环引用所以没事,而上一个例子,关闭了gc,但是出现了循环引用,而引用计数机制只会根据引用计数来判断,而发现引用计数不为0,所以就一直傻傻地不回收,程序又一直创建新的对象,最终导致内存越用越多。而上一个例子若是开启了gc,那么分代回收计数,就会通过标记--清除的方式将产生循环引用的对象的引用计数减1,而引用计数机制发现引用计数为0了,那么就会将对象回收掉。所以这个引用计数机制到底算不算垃圾回收机制的一种呢?你要说算吧,我把gc关闭了,引用计数机制还可以发挥作用,你要说不算吧,它确实是负责判定对象是否应该被回收的唯一标准,所以该怎么说就具体看情况吧。

gc.isenabled():判断gc是否开启

import gc


print(gc.isenabled())  # True
gc.disable() 
print(gc.isenabled())  # False

gc.collect():立刻触发垃圾回收

我们说,垃圾回收触发是需要条件的,比如0代链表,清理零代链表的时候,需要对象的个数count大于阈值threshold(默认是700),但是这个函数可以强制触发垃圾回收。

gc.get_threshold():返回每一代的阈值

import gc


print(gc.get_threshold())  # (700, 10, 10)
# 700:零代链表的对象超过700个,触发垃圾回收
# 10:零代链表,垃圾回收10次,会清理一代链表
# 10:一代链表,垃圾回收10次,会清理二代链表

gc.set_threshold():设置每一代的阈值

import gc


gc.set_threshold(1000, 100, 100)
print(gc.get_threshold())  # (1000, 100, 100)

gc.get_count():查看每一代的值达到了多少

import gc


print(gc.get_count())  # (44, 7, 5)

gc.get_stats():返回每一代的具体信息

from pprint import pprint
import gc


pprint(gc.get_stats())
"""
[{'collected': 316, 'collections': 62, 'uncollectable': 0},
 {'collected': 538, 'collections': 5, 'uncollectable': 0},
 {'collected': 0, 'collections': 0, 'uncollectable': 0}]
"""

gc.get_objects():返回被垃圾回收器追踪的所有对象,一个列表

gc.is_tracked(obj):查看对象obj是否被垃圾收集器追踪

import gc


a = 1
b = []

print(gc.is_tracked(a))  # False
print(gc.is_tracked(b))  # True

# 我们说只有那些可能会产生循环引用的对象才会被垃圾回收器跟踪

gc.get_referrers(obj):返回所有引用了obj的对象

gc.get_referents(obj):返回所有被obj引用了的对象

gc.freeze():冻结所有被垃圾回收器跟踪的对象并在以后的垃圾回收中不被处理

gc.unfreeze():取消所有冻结的对象,让它们继续参数垃圾回收

gc.get_freeze_count():获取冻结的对象个数

import gc


# 不需要参数,会自动找到被垃圾回收器跟踪的对象
gc.freeze()
# 说明有很多内置对象在被跟踪,被我们冻结了
print(gc.get_freeze_count())  # 24397

b = []
gc.freeze()
# 只要这里比上面多1个就行
print(gc.get_freeze_count())  # 24398

# 取消冻结
gc.unfreeze()
print(gc.get_freeze_count())  # 0

gc.get_debug():获取debug级别

import gc


print(gc.get_debug())  # 0

gc.set_debug():设置debug级别

import gc


"""
DEBUG_STATS - 在垃圾收集过程中打印所有统计信息
DEBUG_COLLECTABLE - 打印发现的可收集对象
DEBUG_UNCOLLECTABLE - 打印unreachable对象(除了uncollectable对象)
DEBUG_SAVEALL - 将对象保存到gc.garbage(一个列表)里面,而不是释放它
DEBUG_LEAK - 对内存泄漏的程序进行debug (everything but STATS).
    
"""
class A:
    pass


class B:
    pass


a = A()
b = B()

gc.set_debug(gc.DEBUG_STATS | gc.DEBUG_SAVEALL)
print(gc.garbage)  # []
a.b = b
b.a = a
del a, b
gc.collect()  # 强制触发垃圾回收
# 下面都是自动打印的
"""
gc: collecting generation 2...
gc: objects in each generation: 123 3732 20563
gc: objects in permanent generation: 0
gc: done, 4 unreachable, 0 uncollectable, 0.0000s elapsed
gc: collecting generation 2...
gc: objects in each generation: 0 0 24249
gc: objects in permanent generation: 0
gc: done, 0 unreachable, 0 uncollectable, 0.0150s elapsed
gc: collecting generation 2...
gc: objects in each generation: 525 0 23752
gc: objects in permanent generation: 0
gc: done, 7062 unreachable, 0 uncollectable, 0.0000s elapsed
gc: collecting generation 2...
gc: objects in each generation: 0 0 21941
gc: objects in permanent generation: 0
gc: done, 4572 unreachable, 0 uncollectable, 0.0000s elapsed
"""
print(gc.garbage)
# [<__main__.A object at 0x0000020CFDB50250>, <__main__.B object at 0x0000020CFDB50340>, {'b': <__main__.B object at 0x0000020CFDB50340>}, {'a': <__main__.A object at 0x0000020CFDB50250>}]

17.6 总结

尽管python采用了最经典的(最土的)的引用计数来作为自动内存管理的方案,但是python采用了多种方式来弥补引用计数的不足,内存池的大量使用,标记--清除(分代技术采用的去除循环引用的引用计数的方式)垃圾收集技术都极大地完善了python的内存管理(包括申请、回收)机制。尽管引用计数机制需要花费额外的开销来维护引用计数,但是现在这个年代,这点内存算个啥。而且引用计数也有好处,不然早就随着时代的前进而被扫进历史的垃圾堆里面了。首先引用计数真的很方便,很直观,对于很多对象引用计数能够直接解决,不需要什么复杂的操作;另外引用计数将垃圾回收的开销分摊在了整个运行时,这对于python的响应是有好处的。

当然内存管理和垃圾回收是一门给常精细和繁琐的技术,有兴趣的话各位可以自己大刀阔斧的冲进python的源码中自由翱翔。

技术图片

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

标签:resize   ++   new   共存   状态   如何   需要   检测   合成   

原文地址:https://www.cnblogs.com/traditional/p/12202429.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!