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

17.0 序

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

17.1 内存管理架构

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

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

在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会自动侦测其所需的内存空间大小。c#

第一层所提供的内存管理接口的功能是很是有限的,若是建立一个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的源码中自由翱翔。

相关文章
相关标签/搜索