1. JEMalloc分配算法
Netty的PooledByteBuf
采用与jemalloc一致的内存分配算法。可用这样的情景类比,想像一下当前电商的配送流程。当顾客采购小件商品(比如书籍)时,直接从同城仓库送出;当顾客采购大件商品(比如电视)时,从区域仓库送出;当顾客采购超大件商品(比如汽车)时,则从全国仓库送出。Netty的分配算法与此相似,可参见下图:
稍有不同的是:在Netty中,小件商品和大件商品都首先从同城仓库(ThreadCache-tcache)送出;如果同城仓库没有,则会从区域仓库(Arena)送出。
对于商品分类,Netty根据每次请求分配内存的大小,将请求分为如下几类:
注意以下几点:
- 内存分配的最小单位为16B。
- < 512B的请求为Tiny,< 8KB(PageSize)的请求为Small,<= 16MB(ChunkSize)的请求为Normal,> 16MB(ChunkSize)的请求为Huge。
- < 512B的请求以16B为起点每次增加16B;>= 512B的请求则每次加倍。
- 不在表格中的请求大小,将向上规范化到表格中的数据,比如:请求分配511B、512B、513B,将依次规范化为512B、512B、1KB。
1.1 Arena
为了提高内存分配效率并减少内部碎片,jemalloc算法将Arena切分为小块Chunk,根据每块的内存使用率又将小块组合为以下几种状态:QINIT,Q0,Q25,Q50,Q75,Q100。Chunk块可以在这几种状态间随着内存使用率的变化进行转移,内存使用率和状态转移可参见下图:
其中横轴表示内存使用率(百分比),纵轴表示状态,可知:
- QINIT的内存使用率为[0,25)、Q0为(0,50)、Q100为[100,100]。
- Chunk块的初始状态为QINIT,当使用率达到25时转移到Q0状态,再次达到50时转移到Q25,依次类推直到Q100;当内存释放时又从Q100转移到Q75,直到Q0状态且内存使用率为0时,该Chunk从Arena中删除。注意极端情况下,Chunk可能从QINIT转移到Q0再释放全部内存,然后从Arena中删除。
1.2 Chunk和Page
虽然已将Arena切分为小块Chunk,但实际上Chunk是相当大的内存块,在jemalloc中建议为4MB,Netty默认使用16MB。为了进一步提高内存利用率并减少内部碎片,需要继续将Chunk切分为小的块Page。一个典型的切分将Chunk切分为2048块,Netty正是如此,可知Page的大小为:16MB/2048=8KB。一个好的内存分配算法,应使得已分配内存块尽可能保持连续,这将大大减少内部碎片,由此jemalloc使用伙伴分配算法尽可能提高连续性。伙伴分配算法的示意图如下:
图中最底层表示一个被切分为2048个Page的Chunk块。自底向上,每一层节点作为上一层的子节点构造出一棵满二叉树,然后按层分配满足要求的内存块。以待分配序列8KB、16KB、8KB为例分析分配过程(每个Page大小8KB):
- 8KB--需要一个Page,第11层满足要求,故分配2048节点即Page0;
- 16KB--需要两个Page,故需要在第10层进行分配,而1024的子节点2048已分配,从左到右找到满足要求的1025节点,故分配节点1025即Page2和Page3;
- 8KB--需要一个Page,第11层满足要求,2048已分配,从左到右找到2049节点即Page1进行分配。
分配结束后,已分配连续的Page0-Page3,这样的连续内存块,大大减少内部碎片并提高内存使用率。
1.3 SubPage
Netty中每个Page的默认大小为8KB,在实际使用中,很多业务需要分配更小的内存块比如16B、32B、64B等。为了应对这种需求,需要进一步切分Page成更小的SubPage。SubPage是jemalloc中内存分配的最小单位,不能再进行切分。SubPage切分的单位并不固定,以第一次请求分配的大小为单位(最小切分单位为16B)。比如,第一次请求分配32B,则Page按照32B均等切分为256块;第一次请求16B,则Page按照16B均等切分为512块。为了便于内存分配和管理,根据SubPage的切分单位进行分组,每组使用双向链表组合,示意图如下:
其中每组的头结点head只用来标记该组的大小,之后的节点才是实际分配的SubPage节点。需要注意的是,这些节点正是上一节中满二叉树的叶子节点即一个Page。
至此,已介绍完jemalloc的基本思想,关于具体实现细节,可跳转: