背景:

为了较好的学习与理解python的内存管理,先从设计框架了解,然后再拆分为小的知识块,针对知识块再逐步理解。说道内存管理,应该说一则是内存的创建分配管理,二则为内存的回收,即垃圾回收。

垃圾回收

如思维导图:




python thresh python threshold 类型_python内存管理


分代回收

gc.set_threshold(threshold0[, threshold1[, threshold2]])

内存创建与管理

C源码地址github.com


小空间内存池

为了避免大量的执行malloc和free的操作,即避免操作系统频繁的再用户态和内核态之间进行切换。因此python引入内存池机制,用于管理小内存块的申请与释放,小于512字节的由内存池管理,超过的由标准的C分配器处理。整个管理小内存的内存池可以视为一个抽象的层次结构,由block pool arean组成。首先我们来看最小的结构block。

block(块)

块 是一定大小的内存块。每个块只能保留一个固定大小的Python对象。块的大小可以从8字节到512字节不等,为了在当前主流的32位和64位平台上获得最佳的性能,必须是8字节的倍数(即与8字节对齐)。为了方便起见,这些块被分为64个尺寸类别。


# obmalloc.c文件里定义如下
#define ALIGNMENT               8               /* must be 2^N */
#define ALIGNMENT_SHIFT         3

#define SMALL_REQUEST_THRESHOLD 512
#define NB_SMALL_SIZE_CLASSES   (SMALL_REQUEST_THRESHOLD / ALIGNMENT)


当申请的内存小于512字节,python可以使用不同的级别的block满足对内存的需求,当申请的内存大小超过这个限制时,python会将内存的申请请求转发给第一层的内存管理机制,即PyMem_API来处理了。根据SMALL_REQUEST_THRESHOLDALIGNMENT, 可以由此得到不同种类的block的size类别。明细表如下:


* For small requests we have the following table:
 *
 * 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
 *
 *      0, SMALL_REQUEST_THRESHOLD + 1 and up: routed to the underlying
 *      allocator.
 */


举例说明,当申请内存大小为35字节的内存时,实际上PyObject_Malloc从内存池中划分的内存是40字节的一个block, 从 size class index 为4的pool中划出.转换公式:


# size class index 转换到size class
/* Return the number of bytes in size class I, as a uint. */
#define INDEX2SIZE(I) (((uint)(I) + 1) << ALIGNMENT_SHIFT)

// size class 转换到size class index
size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;


block只是概念,并不是实体,是具有一定大小的内存,为了管理block,有了pool的概念。

pool(池)

大小相同的块的集合称为池。通常,池的大小等于系统内存页的大小,即4KB。将池限制为固定大小的块有助于碎片化。如果一个对象被破坏,内存管理器可以用一个相同大小的新对象填充这个空间.池对应的结构为:


/* When you say memory, my mind reasons in terms of (pointers to) blocks */
typedef uint8_t block;

/* Pool for small blocks. */
struct pool_header {
    union { block *_padding;
            uint count; } ref;          /* number of allocated blocks 已分配的block数量   */
    block *freeblock;                   /* pool's free list head    链表结构,指向下一个可用的block     */
    struct pool_header *nextpool;       /* next pool of this size class  双链表,下一个池  */
    struct pool_header *prevpool;       /* previous pool   指向上一个池    ""        */
    uint arenaindex;                    /* index into arenas of base adr  创建池时对应的arean地址 */
    uint szidx;                         /* block size class index       对应的block的size class index */
    uint nextoffset;                    /* bytes to virgin block     下一个可用的block偏移量    */
    uint maxnextoffset;                 /* largest valid nextoffset   最后一个block距离开始位置的距离   */
};
typedef struct pool_header *poolp;


pool在运行的任何时刻, 总处于三个状态中的一种:

  • used状态: 部分block使用, 不是full也不是empty;
  • full: pool中所有的block都被使用,;
  • empty: pool中所有的block都未被使用。

针对used状态,为了有效地管理池,Python使用了一个名为usedpools的附加数组。它存储指向按类分组的池的指针。由于相同块大小的所有池都链接在一起。要遍历它们,我们只需要知道列表的开头。如果没有这样大的池,那么将在第一个内存请求时创建一个新池。如图:


python thresh python threshold 类型_sed_02


arena

pool的集合,每个arena 的大小都有一个默认的值256KB,一个arena中容纳的pool的个数就是ARENA_SIZE / POOL_SIZE = 64个,arena结构如下:


[obmalloc.c],
#define ARENA_SIZE              (256 << 10)     /* 256KB */

# 结构
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;
};


如图:


python thresh python threshold 类型_python thresh_03


arena是用来管理一组pool的集合的, arena_object的作用看上去和pool_header的作用是一样的, 但还是有一点差别的, pool_header与其管理的pool是一块连续的内存; 而arena_object与其管理的内存则是分离的,如图:


python thresh python threshold 类型_python thresh_04


当pool_header申请时, 它所管理的block集合的内存一定也被申请了; 但是当申请arena_object时, 它所管理的pool集合的内存则没有被申请, 换句话说, arena_object和pool集合还需要建立关系。

当一个arena的arena_object没有与pool集合建立联系时, 这时的arena处于"未使用"状态; 建立关系后, arena就转为"可用"状态. 对每一个状态, 都有一个 arena 的链表. "未使用" 的arena的链表表头是unused_arena_objects. 此时的arena与arena之间通过nextarena连接, 是一个单向链表; 而 "可用" 的arena的表头是usable_arenas, arena 与 arena 通过nextarenaprevarena连接, 是一个双向链表, 如图展示了多个arena的可能状态。


python thresh python threshold 类型_sed_05


这里一定要注意有两个链表,unused_arena_objects属于单链表,usable_arenas属于双链表。

在程序运行时,当需要申请内存时,首先会检查unused_arena_objects链表中是否还有"未使用"的arena。 如果unused_arena_objects中存在未使用的arena, 那么直接从中取出一个arena, 调整unused_arena_objects指针, 与抽取的arena断绝联系. python申请一块大小为ARENA_SIZE(256KB)的内存, 将申请的内存地址复制给arena的address.这个地址就是维护pool的集合用的. 这块256KB的内存就是pool的容身之处, arena_object和pool集合建立了联系, 具备了 "可用" 的条件, 加入了usable_arenas链表 。源码如下:


/* Allocate a new arena.  If we run out of memory, return NULL.  Else
 * allocate a new arena, and return the address of an arena_object
 * describing the new arena.  It's expected that the caller will set
 * `usable_arenas` to the return value.
 */
static struct arena_object*
new_arena(void)
{
    struct arena_object* arenaobj;
    uint excess;        /* number of bytes above pool alignment */
    void *address;
    static int debug_stats = -1;

    if (debug_stats == -1) {
        char *opt = Py_GETENV("PYTHONMALLOCSTATS");
        debug_stats = (opt != NULL && *opt != '0');
    }
    if (debug_stats)
        _PyObject_DebugMallocStats(stderr);

    if (unused_arena_objects == NULL) {
        uint i;
        uint numarenas;
        size_t nbytes;

        /* Double the number of arena objects on each allocation.
         * Note that it's possible for `numarenas` to overflow.
         */
        numarenas = maxarenas ? maxarenas << 1 : INITIAL_ARENA_OBJECTS;
        if (numarenas <= maxarenas)
            return NULL;                /* overflow */
#if SIZEOF_SIZE_T <= SIZEOF_INT
        if (numarenas > SIZE_MAX / sizeof(*arenas))
            return NULL;                /* overflow */
#endif
        nbytes = numarenas * sizeof(*arenas);
        arenaobj = (struct arena_object *)PyMem_RawRealloc(arenas, nbytes);
        if (arenaobj == NULL)
            return NULL;
        arenas = arenaobj;

        /* We might need to fix pointers that were copied.  However,
         * new_arena only gets called when all the pages in the
         * previous arenas are full.  Thus, there are *no* pointers
         * into the old array. Thus, we don't have to worry about
         * invalid pointers.  Just to be sure, some asserts:
         */
        assert(usable_arenas == NULL);
        assert(unused_arena_objects == NULL);

        /* Put the new arenas on the unused_arena_objects list. */
        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. */
    assert(unused_arena_objects != NULL);
    arenaobj = unused_arena_objects;
    unused_arena_objects = arenaobj->nextarena;
    assert(arenaobj->address == 0);
    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;
    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);
    excess = (uint)(arenaobj->address & POOL_SIZE_MASK);
    if (excess != 0) {
        --arenaobj->nfreepools;
        arenaobj->pool_address += POOL_SIZE - excess;
    }
    arenaobj->ntotalpools = arenaobj->nfreepools;

    return arenaobj;
}


总结

  1. block会被垃圾回收吗,垃圾回收针对的对象是什么?
  2. pool 的状态
  3. arena的维护
  4. 垃圾回收中的 引用计算 分代回收 标记-清除