netty 默认使用池化,堆外内存
// 创建内存分配器,使用池化,堆外。正常使用 netty 时,不需要自行创建 PooledByteBufAllocator allocator = new PooledByteBufAllocator(true); // 分配内存 ByteBuf buffer = allocator.buffer(512); // 归还内存 buffer.release();
内存规格
512B,8K
< 512B tiny
521B ~ 8k small
>8K normal
起始在这
PooledByteBufAllocator
为了避免多线程竞争,使用 PoolThreadCache,每个线程有单独的内存区域,为简化说明,这里只考虑堆外内存。
final class PoolThreadCache { // 内存分配之后,被释放,则先放到缓存 final PoolArena<ByteBuffer> directArena; private final MemoryRegionCache<ByteBuffer>[] tinySubPageDirectCaches; private final MemoryRegionCache<ByteBuffer>[] smallSubPageDirectCaches; private final MemoryRegionCache<ByteBuffer>[] normalDirectCaches; }
简单来看,PoolArena 组合了 PoolChunk 和 PoolSubpage
abstract class PoolArena<T> implements PoolArenaMetric { private final PoolSubpage<T>[] tinySubpagePools; private final PoolSubpage<T>[] smallSubpagePools; private final PoolChunkList<T> q050; private final PoolChunkList<T> q025; private final PoolChunkList<T> q000; private final PoolChunkList<T> qInit; private final PoolChunkList<T> q075; private final PoolChunkList<T> q100; }
PoolChunk
一个 chunk 16M
一个 page 8K
利用一棵完全二叉树,把连续的 16M 内存分段,不同深度的节点代表不同粒度大小的内存。
用数组表示完全二叉树,数组有 4096 个元素,第 0 个元素不使用,用 1-4095 来对应完全二叉树的节点
这个数组就是 byte[] memoryMap,数组元素的值是深度,树的深度和可分配内存的大小有关联。
memoryMap[1] = 0
memoryMap[2] = 1
memoryMap[3] = 1
memoryMap[4] = 2
memoryMap[5] = 2
memoryMap[6] = 2
memoryMap[7] = 2
深度 -> 内存
0 -> 16M
1 -> 8M
2 -> 4M
3 -> 2M
4 -> 1M
5 -> 512K
6 -> 256K
7 -> 128K
8 -> 64K
9 -> 32K
10 -> 16K
11 -> 8K
根据申请内存大小,算出二叉树的深度,如果申请 8K 内存,则从 11 层寻找节点
第一次分配,把 2048 号节点分配出去,然后更新 2048 号节点父节点 1024 号 ,memoryMap[1024] = 11,表示该节点只能分配 8K 大小的内存了
// io.netty.buffer.PoolChunk#allocate boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) { final long handle; // 申请的内存,规格化之后大于 8K if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize handle = allocateRun(normCapacity); } else { handle = allocateSubpage(normCapacity); } if (handle < 0) { return false; } ByteBuffer nioBuffer = cachedNioBuffers != null ? cachedNioBuffers.pollLast() : null; initBuf(buf, nioBuffer, handle, reqCapacity); return true; } private long allocateRun(int normCapacity) { // 完全二叉树的深度 int d = maxOrder - (log2(normCapacity) - pageShifts); // 节点的序号 int id = allocateNode(d); if (id < 0) { return id; } freeBytes -= runLength(id); return id; }
PoolSubpage
小于 8K 的分配,有
tinySubpagePools 的规格分别为 16, 32, 64, ... ,496,共 32 个元素,最后一个元素应该没有用到
smallSubpagePools 规格分为 512, 1024, 2048, 4096,共 4 个元素
16M 内存按 8K 分页,
申请的内存大于 8K,直接分配多个连续的页;
申请的内存小于 8K,则把页按规格分段,small 分为 512,1024,2048,4096,tiny 分为 16,32,64,96,。。。
需要明确的是,一个内存页只能分配固定规格的内存片段
分配内存的最小单位是 16 字节,一页大小 8K,所以最多需要 8K/16=512 位来标识内存是否分配,即 8 个 long 型。
分配一页,怎么标记一页已被分配?
二叉树数组 memoryMap 的元素值,深度改变了,表明该页被分配。
分配一小段,怎么标记一小段已被分配?
用 8 个 long,一位表示一段被分配。
小块内存的释放,把内存放到 cache 的队列中
io.netty.buffer.PoolThreadCache.MemoryRegionCache#add
再次分配内存时,从缓存中取
io.netty.buffer.PoolThreadCache.MemoryRegionCache#allocate
内存的管理,非常复杂,后面再慢慢看吧!