内存分配的目标
- 减少内存分配和回收的时间
- 减少内部和外部碎片,提高利用率
内存分配的演进
固定大小的Page:
不同大小的Page:
jemalloc(Netty内存分配算法)的核心:Page组合+缓存
Netty内存分配的架构
整合上面提到的架构,通过Page细粒度划分、Page复用、使用率平衡、链表+满二叉树等手段达到缩短响应时间、减少内存碎片的目的。
分配流程
- 线程私有缓存查找
尝试从线程私有缓存中查找已释放的空闲块
内存块使用完之后,不会马上归还,而是缓存起来,每一次请求先从缓存获取。
优势:
缓存+复用,提高响应速度
- 线程共享缓存查找
如果是tiny或者small,尝试从缓存中查找已释放的空闲块
优势:
缓存+复用,提高响应速度
- 找到一个可以分配内存的chunk
从链表中找到一个chunk(Page的集合),在chunk里面分配page。chunk是netty申请内存的基本单位,一个chunk是16M。
把使用率不同的chunk归类在6个链表中,每一次从中间链表开始查找,找到足够空间的chunk就返回
chunk使用率超出当前链表的阈值时,会向左或向右移动到合适的链表中
优势:
如果每次都从高使用率的chunk开始分配,空间不足的概率较高,需要多次才能分配成功;
如果每次都从低使用率的chunk开始分配,整体空间利用率很低,内存碎片较多;
按使用率分类,每次从使用率中等的Chunk分配,保证整体使用率平衡。
- 从chunk中分配一个Page
进入chunk中找到一个page,如果需求小于8K会拆分page、分配subpage并且把剩下的放入缓存,如果需求大于8K会分配连续的多个page。
为了减少chunk里面meta信息的占用空间,实际的存储内容为每个节点一个数字,初始值:
分配后:
内存分配基本单位是一个8K的page(树的叶节点),如果需求小于8K,其实是把某个page拆分成tiny/small的subpage,如果大于8K,其实是分配多个page,这样可拆可合的方式满足了不同大小的内存分配需求,减小碎片;
不同大小的内存块分别保存在不同的队列中,从队列头部获取;pageSize有下面几种:
大于8K的分配,比如16K:
计算出内存块应该在倒数第二层,然后从左子节点到右子节点查找,由于每个节点都记录了所在子树是否已被分配。
分配出去后,递归标记所有父节点,表示当前节点及子树已被分配。
小于8K的分配,比如512B:
通过上面的步骤找到叶子结点,一个8K的Page;
把Page拆分成16个大小为512B的SubPage,全部添加到small subpage list里面,并把第一个分配出去,剩下的都缓存下来,等下一次分配。
优势:
Page组合和拆分,更灵活和细粒度的Page大小,减少内存碎片
总结
- 通过PooledByteBufAllocator进入PoolArena尝试分配内存,每个线程在第一次分配内存的时候从Arena数组中选择一个固定的Arena并绑定,在整个生命周期都只与该Arena交互。
- PoolArena最重要的部分是PoolChunk,由六个PoolChunkList把所有的PoolChunk组合起来,每个PoolChunkList负责管理一类PoolChunk,由PoolChunk的使用率决定其属于哪一个List,当其使用率达到临界值的时候,会从一个List移动到另一个List,一般情况下,一个PoolChunk产生于qInit,最终会到达q100。
- Netty向操作系统申请存储空间的单位是PoolChunk,默认大小为16M。PoolChunk由2048个Page组成,每个Page的默认大小为8K,如图中右下所示。Netty把所有的Page保存在一个长度为2048的数组里面,并将其抽象为一棵满二叉树,方便记录内存是否已使用。
- 大于等于8K且小于等于16M的内存分配请求统称为Normal请求,此类请求会精确到Page的层次;大于16M的内存分配请求称为Huge请求,会使用非池化的方式直接分配;大于等于512个字节但小于8K的请求称为Small请求,大于等于16字节但小于512字节的请求称为Tiny请求,这两类请求可能会把一个Page继续拆分成更小的内存块,并且将拆出来的内存块的引用保存在图中左上角所示的tinySubpagePools和smallSubpagePools中。各类内存块的分配流程将会在后文的源码分析中有所展示。
- PoolThreadCache是Netty版本的ThreadLocalCache,在不同线程间隔离,用于复用线程已申请过的内存。每一次的内存分配请求都会从PoolThreadCache开始,可能会在(tiny/small)SubpagePools结束,也可能继续到PoolChunk中才会结束。
- 内存块划分的力度越小有利于减少外部和内部碎片,但会提高内存管理难度,Netty内存管理(或者说jemalloc)是两者的折衷。