netty 内存相关,太复杂了

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

内存的管理,非常复杂,后面再慢慢看吧!

原文地址:https://www.cnblogs.com/allenwas3/p/12350442.html