Python内存管理--内存池

本文章纯粹是个人学习的理解,难免存在错误和纰漏,欢迎大家指正。

前言

众所周知的是,在Python中一切对象。由于Python动态的特性,在运行过程中会创建大量的对象,即使是临时变量,这些对象也都是分配在heap中的。如果每次使用对象都调用malloc,而当引用计数为0时就调用free进行释放的话。大量的系统调用会使程序的效率十分低下。并且频繁分配和释放小对象,也会出现大量的内存碎片。所以抽空学习了一下Python是如何管理自己内存的。
由于内存管理设计的内容比较多,包含内存池,对象池,GC等。这次学习主要是内存池的内容,对象池和GC的内容先挖坑,后面慢慢填。

Overview of Python Memory management

结合Python官方文档和 obmalloc.c中的源码。在Python的最底层,Python原始的内存分配器确保了在私有堆中有足够的空间来存储Python相关的数据。而各个对象特定的内存分配器则在原始内存分配器分配的内存上给各个对象分配内存。而Python的内存管理器则确保这些内存分配器在所分配的对空间上运行。
下图是CPython内存管理的布局

CPython内存布局

为了减少小对象内存分配所带来的开销——内存碎片和大量系统调用——Python针对小对象(小于512字节)的内存分配采用了内存池来进行管理。而对超过512字节的对象直接使用标准C的内存分配器。对小对象内存的分配器Python进行了3个等级的抽象——arena, poolblock

Block

Block是Python内存管理器的最小单元。Block是特定大小的内存块。而在每个block中存储着一个Python对象。Block的大小只能是[8, 16, 24, 32, 40, 48, ... , 504, 512]个字节,而以8字节为一个单位,则是为了字节对齐的需要。如果一个对象申请的内存为13个字节,那么Python将会为这个对象分配一个16字节的block, 以此类推。

Pool

一系列Block则组成一个Pool。在一个Pool中,所有Block的大小都是一样的,也就是说,Pool是一种Block的容器。每个Pool为4K的大小(也就是一个虚拟内存页的大小)。一个小对象被销毁后,其内存不会马上归还给系统,而是在Pool中被管理者,用于分配给后面申请的内存的对象。obmalloc.c 关于pool的数据结构描述如下

1
2
3
4
5
6
7
8
9
10
11
12
/* Pool for small blocks*/
struct pool_header {
union { block *_padding;
uint count; } ref; /* number of allocated blocks */
block *freeblock; /* pool's free list head */
struct pool_header *nextpool; /* next pool of this size class */
struct pool_header *prevpool; /* previous pool "" */
uint arenaindex; /* index into arenas of base adr */
uint szidx; /* block size class index */
uint nextoffset; /* bytes to virgin block */
uint maxnextoffset; /* largest valid nextoffset */
};

拥有相同Block大小的Pool通过双向链表连接起来(nextpool, prevpool),确保能够在现有的pool中快速找到可以分配的block块。szidx记录着Pool保存的Block的大小。ref.count则记录着Pool已经被使用的Block的数量。freeblock则指向Pool中可用Block的单向链表。
实际在Pool中对Block并没有使用实际的单向链表结构,而是在对应的Block中记录着下一个可用Block的地址,确保了内存的高效利用。

Pool freeblock示意图

有些同学难免有些疑问,为何在Pool第一次创建的时候,就有被分配的块了呢?这是因为Python只在没有Pool可以分配对应大小的Block的时候才从可用Pool列表中取出一个Pool来分配Block,所以这个被分配的Block就是请求的Block。

当freeblock指向链表的末尾时,nextoffset才会增加,Pool中的untouched部分的内存才会被使用到。

另外,Python使用了一个数组usedpools来管理使用中的Pool。
usedpools示意图

Arena

Arena则是Python从系统分配申请和释放的单位,有256KB,每个Arena中包含了64个Pool。Arena的数据结构如下

1
2
3
4
5
6
7
8
9
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也是使用双向链表来进行管理的。这个双向链表成为usable_arenas, 这个双向链表按Arena中可使用的Pool数量升序排列。Python在分配Pool的时候优先选择可用Pool数量少的Arena进行内存分配。为何使用这个策略?这是因为Python只在Arena中所有的Pool全为空时才释放Arena中的内存,如果选择可用Pool数量最多的Arena的话,大量的内存也会被占用,导致内存不会被销毁。

Summary

Python对小对象的内存管理通过预分配的方式来进行管理。每个Pool中分配固定大小的Block来为对象分配内存。并且通过Pool和Arena的代码中,我们看到Python对内存管理奉行着的哲学: “Nevet to touch a piece of memory until ti’s actually needed”。但是由于Python对内存的申请和释放是以Arena为单位的,所以,如果存在一些对象迟迟没有被释放,那么此时Arena将不会被释放掉,一直占用着这块内存。

Author: lisupy
Link: http://lisupy.github.io/2020/05/25/Python内存管理-内存池/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
支付宝打赏
微信打赏