从netty 4开始,netty加入了内存池管理,采用内存池管理比普通的new ByteBuf性能提高了数(2)十倍。相信有些朋友会和我一样,对他的实现方式很感兴趣。这里把学习netty内存池的过程记录下来,与大家一起分享。
首先给大家介绍的是PoolChunk, 该类主要负责内存块的分配与回收,首先来看看两个重要的术语:
page: 可以分配的最小的内存块单位。
chunk: 一堆page的集合。
下面我们用一张图直观的看下PoolChunk是如何管理内存的:
上图中是一个默认大小的chunk, 由2048个page组成了一个chunk,一个page的大小为8192, chunk之上有11层节点,最后一层节点数与page数量相等。每次内存分配需要保证内存的连续性,这样才能简单的操作分配到的内存,因此这里构造了一颗完整的平衡二叉树,所有子节点的管理的内存也属于其父节点。如果我们想获取一个8K的内存,则只需在第11层找一个可用节点即可,而如果我们需要16K的数据,则需要在第10层找一个可用节点。如果一个节点存在一个已经被分配的子节点,则该节点不能被分配,例如我们需要16K内存,这个时候id为2048的节点已经被分配,id为2049的节点未分配,就不能直接分配1024这个节点,因为这个节点下的内存只有8K了。
通过上面这个树结构,我们可以看到每次内存分配都是8K*(2^n), 比如需要24K内存时,实际上会申请到一块32K的内存。为了分配一个大小为chunkSize/(2^k)的内存段,需要在深度为k的层从左开始查找可用节点。如想分配16K的内存,chunkSize = 16M, 则k=10, 需要从第10层找一个空闲的节点分配内存。
如何高效的从这么多page中分配到指定的内存呢。来看看下面这个图:
这个图与上图结构一致,不同的是上方的二叉树的值为当前的层数,两张图和起来用一个数组memoryMap表示,上面的图中的数字表示数组的index,下面的图中的数字表示当前节点及其子节点可以分配的层的高度。如对于id=512的节点,其深度为9,则:
1)memoryMap[512] = 9,则表示其本身到下面所有的子节点都可以被分配;
2)memoryMap[512] = val (从10到11), 则表示512节点下有子节点已经分配过,则该节点不能直接被分配,而其子节点中的第val和val以下层还存在未分配的节点;
3)memoryMap[512] = 12 (即总层数 + 1), 可分配的深度已经大于总层数, 则该节点下的所有子节点都已经被分配。
下面我们在从源码分析下PoolChunk是如何实现的,首先看看它的构造方法:
PoolChunk(PoolArena<T> arena, T memory, int pageSize, int maxOrder, int pageShifts, int chunkSize) {
unpooled = false;
this.arena = arena;
// memory是一个容量为chunkSize的byte[](heap方式)或ByteBuffer(direct方式)
this.memory = memory;
// 每个page的大小,默认为8192
this.pageSize = pageSize;
// 13, 2 ^ 13 = 8192
this.pageShifts = pageShifts;
// 默认11
this.maxOrder = maxOrder;
// 默认 8192 << 11 = 16MB
this.chunkSize = chunkSize;
// 12, 当memoryMap[id] = unusable时,则表示id节点已被分配
unusable = (byte) (maxOrder + 1);
// 24, 2 ^ 24 = 16M
log2ChunkSize = log2(chunkSize);
// -8192
subpageOverflowMask = ~(pageSize - 1);
freeBytes = chunkSize;
assert maxOrder < 30 : "maxOrder should be < 30, but is: " + maxOrder;
// 2048, 最多能被分配的Subpage个数
maxSubpageAllocs = 1 &l