Netty的PooledByteBuf采用与jemalloc[jemalloc 在 2005 年首次作为 FreeBSD libc 分配器使用]一致的内存分配算法。

jemalloc

可用这样的情景类比,想像一下当前电商的配送流程。当顾客采购小件商品(比如书籍)时,直接从同城仓库送出;当顾客采购大件商品(比如电视)时,从区域仓库送出;当顾客采购超大件商品(比如汽车)时,则从全国仓库送出。

稍有不同的是:在Netty中,小件商品和大件商品都首先从同城仓库(ThreadCache-tcache)送出;如果同城仓库没有,则会从区域仓库(Arena)送出。

Netty内存大小分类

Netty根据每次请求分配内存的大小,将请求分为如下几类:

分类 子分类 大小
Tiny/Small Tiny 16B
32B

480B
496B
Tiny/Small Small 512B
1KB

2KB
4KB
Normal 8KB
16KB

8MB
16MB
Huge 32KB
64KB
  1. 内存分配的最小单位为16B。
  2. < 512B的请求为Tiny,< 8KB(PageSize)的请求为Small,<= 16MB(ChunkSize)的请求为Normal,> 16MB(ChunkSize)的请求为Huge。
  3. < 512B的请求以16B为起点每次增加16B;>= 512B的请求则每次加倍。
  4. 不在表格中的请求大小,将向上规范化到表格中的数据,比如:请求分配511B、512B、513B,将依次规范化为512B、512B、1KB。

Arena

arena是jemalloc的总的管理块,一个进程中可以有多个arena,为了提高内存分配效率并减少内部碎片,jemalloc算法将Arena切分为小块Chunk,根据每块的内存使用率又将小块组合为以下几种状态:QINIT,Q0,Q25,Q50,Q75,Q100。Chunk块可以在这几种状态间随着内存使用率的变化进行转移。

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

  1. 8KB–需要一个Page,第11层满足要求,故分配2048节点即Page0;
  2. 16KB–需要两个Page,故需要在第10层进行分配,而1024的子节点2048已分配,从左到右找到满足要求的1025节点,故分配节点1025即Page2和Page3;
  3. 8KB–需要一个Page,第11层满足要求,2048已分配,从左到右找到2049节点即Page1进行分配。

分配结束后,已分配连续的Page0-Page3,这样的连续内存块,大大减少内部碎片并提高内存使用率。

SubPage

Netty中每个Page的默认大小为8KB,在实际使用中,很多业务需要分配更小的内存块比如16B、32B、64B等。为了应对这种需求,需要进一步切分Page成更小的SubPage。SubPage是jemalloc中内存分配的最小单位,不能再进行切分。SubPage切分的单位并不固定,以第一次请求分配的大小为单位(最小切分单位为16B)。比如,第一次请求分配32B,则Page按照32B均等切分为256块;第一次请求16B,则Page按照16B均等切分为512块。为了便于内存分配和管理,根据SubPage的切分单位进行分组,每组使用双向链表组合。

Netty中的类

Arena对应的是PoolArena
Chunl对应的是PoolChunk

参考

自顶向下深入分析Netty(十)–JEMalloc分配算法