Netty源码(二)内存池初探

1,165 阅读13分钟

前言

本章学习Netty内存池:

  • 内存分类
  • PoolArena
  • PoolChunkList
  • PoolChunk
  • PoolSubpage

一、内存分类

1、按照大小分类

分类下限上限规格
Tiny>0Byte<=496Byte16B、32B...496B,差值为16的等差数列,共31种规格
Small>=512Byte<=4096Byte512B、1024B、2048B、4096B,比值为2的等比数列,共4种规格
Normal>=8192Byte<=16MB8192B
Huge>16MB-非池化分配

2、按照结构分类

  • Page:一个Page代表8KB内存块,Subpage(可以认为Netty称Page为Subpage)负责分配Tiny和Small规格的小内存。Subpage负责的内存块属于Chunk,是由Chunk创建了Subpage。
  • Chunk:一个Chunk代表16MB内存块,Netty每次向系统申请内存的单位为16MB,超过16MB不使用池化技术。可以认为一个Chunk是Page的集合。

二、PoolArena

PoolArena是内存池的入口,职责如下:

  • 负责向系统申请资源,创建Chunk,管理Chunk。
  • 管理Subpage,通过Subpage分配Tiny和Small规格内存。注意,所有Subpage都是由Chunk创建的,由Chunk放入它对应的Arena的Subpage池中。

PoolArena的成员变量如下,忽略PoolArenaMetric,这些XXXMetric只是需要子类具有一些数据统计功能:

abstract class PoolArena<T> implements PoolArenaMetric {
    // 大小分类
    enum SizeClass {
        Tiny, // 小于等于496B
        Small, // 大于等于512B 小于等于4K
        Normal // 大于等于8K 小于等于16M
    }
    // tiny池大小 = 32
    static final int numTinySubpagePools = 512 >>> 4;
    // 分配器
    final PooledByteBufAllocator parent;
    // 树深度 11
    private final int maxOrder;
    // 一个Page大小 = 8192B = 8K
    final int pageSize;
    // log2(8192) = 13
    final int pageShifts;
    // 一个Chunk大小 = 16MB
    final int chunkSize;
    // -pageSize = -8192
    final int subpageOverflowMask;
    // small池大小 = pageShifts - 9 = 4
    final int numSmallSubpagePools;
    // 直接缓存内存对齐 0
    final int directMemoryCacheAlignment;
    // 直接缓存内存对齐掩码 = directMemoryCacheAlignment - 1 = -1
    final int directMemoryCacheAlignmentMask;
    // 负责规格 16B,32B,48B,...,496B 等差数列 差值16 数组大小32 实际下标从1开始
    // tiny池
    private final PoolSubpage<T>[] tinySubpagePools;
    // 负责规格 512B, 1024B, 2048B, 4096B 等比数列 比值2 数组大小4
    // small池
    private final PoolSubpage<T>[] smallSubpagePools;

    // PoolChunkList
    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;
}

总得来说Arena分为两部分,一部分是PoolChunkList串联的Chunk,另一部分是PoolSubpage池。实际Arena的结构如下图:

解释一下上图:

  • PoolChunkList:每个ChunkList维护了Chunk双向链表,并且所有ChunkList也互相关联构建为一个链表存在于Arena之中。每个新创建的Chunk(向系统声请了16M内存),都会放入qInit中。Chunk使用过程中由于实际使用率的上下波动,会在几个PoolChunkList中来回移动,而PoolChunkList上下限的重叠部分是为了防止Chunk使用率处于临界值频繁来回移动。PoolChunkList的规格如下:
分类下限上限
qInitInteger.MIN_VALUE25%
q0001%50%
q02525%75%
q05050%100%
q07575%100%
q100100%Integer.MAX_VALUE
  • PoolSubpage[32] tinySubpagePools:存放由Chunk分配的PoolSubpage,负责Tiny规格的内存块分配。数组下标1-31,对应16B、32B...496B,共31种规格的Tiny内存块。每个Subpage互相连接为双向链表,头节点为空不存放数据。

  • PoolSubpage[4] smallSubpagePools:存放由Chunk分配的PoolSubpage,负责Small规格的内存块分配。数组下标0-3,对应512B、1024B、2048B、4096B,共4种规格的Small内存块。每个Subpage互相连接为双向链表,头节点为空不存放数据。

看一下Arena的构造方法,主要就是计算各种参数,构建Subpage池和PoolChunkList链。

protected PoolArena(PooledByteBufAllocator parent, int pageSize,
      int maxOrder, int pageShifts, int chunkSize, int cacheAlignment) {
    this.parent = parent;
    // 一些page chunk相关大小计算
    this.pageSize = pageSize; // 8192 B = 8 KB
    this.maxOrder = maxOrder; // 11
    this.pageShifts = pageShifts;// 13
    this.chunkSize = chunkSize; // 16777216 B = 16 MB = pageSize << maxOrder
    directMemoryCacheAlignment = cacheAlignment; // 0
    directMemoryCacheAlignmentMask = cacheAlignment - 1; // -1
    subpageOverflowMask = ~(pageSize - 1); // -pageSize
    // 创建Tiny规格Subpage池
    tinySubpagePools = newSubpagePoolArray(numTinySubpagePools); // 32长度数组
    for (int i = 0; i < tinySubpagePools.length; i ++) {
        tinySubpagePools[i] = newSubpagePoolHead(pageSize); // 构造PoolSubpage头节点
    }
	// 创建Small规格Subpage池
    numSmallSubpagePools = pageShifts - 9; // pageShifts - 9 = 4
    smallSubpagePools = newSubpagePoolArray(numSmallSubpagePools); // 4长度数组
    for (int i = 0; i < smallSubpagePools.length; i ++) {
        smallSubpagePools[i] = newSubpagePoolHead(pageSize); // 构造PoolSubpage头节点
    }
    // 实例化PoolChunkList
    q100 = new PoolChunkList<T>(this, null, 100, Integer.MAX_VALUE, chunkSize);
    q075 = new PoolChunkList<T>(this, q100, 75, 100, chunkSize);
    q050 = new PoolChunkList<T>(this, q075, 50, 100, chunkSize);
    q025 = new PoolChunkList<T>(this, q050, 25, 75, chunkSize);
    q000 = new PoolChunkList<T>(this, q025, 1, 50, chunkSize);
    qInit = new PoolChunkList<T>(this, q000, Integer.MIN_VALUE, 25, chunkSize);
    // PoolChunkList链表连接
    q100.prevList(q075);
    q075.prevList(q050);
    q050.prevList(q025);
    q025.prevList(q000);
    q000.prevList(null);
    qInit.prevList(qInit);
}

PoolArena是个抽象类,需要子类具体实现几个方法。

// 是否使用直接内存
abstract boolean isDirect();
// 创建新的Chunk
protected abstract PoolChunk<T> newChunk(int pageSize, int maxOrder, int pageShifts, int chunkSize);
// 创建非池化的Chunk
protected abstract PoolChunk<T> newUnpooledChunk(int capacity);
// 创建PooledByteBuf
protected abstract PooledByteBuf<T> newByteBuf(int maxCapacity);
// 内存拷贝
protected abstract void memoryCopy(T src, int srcOffset, PooledByteBuf<T> dst, int length);
// 销毁Chunk
protected abstract void destroyChunk(PoolChunk<T> chunk);

这些方法延迟到子类实现是因为泛型T的问题,无法确定使用堆内存byte数组还是直接内存ByteBuffer,所以PoolArena有两个实现类。直接内存实现类DirectArena部分实现如下。

static final class DirectArena extends PoolArena<ByteBuffer> {

    DirectArena(PooledByteBufAllocator parent, int pageSize, int maxOrder,
            int pageShifts, int chunkSize, int directMemoryCacheAlignment) {
        super(parent, pageSize, maxOrder, pageShifts, chunkSize,
                directMemoryCacheAlignment);
    }

    @Override
    boolean isDirect() {
        return true;
    }

    @Override
    protected PoolChunk<ByteBuffer> newChunk(int pageSize, int maxOrder,
            int pageShifts, int chunkSize) {
        if (directMemoryCacheAlignment == 0) {
            return new PoolChunk<ByteBuffer>(this,
                    allocateDirect(chunkSize), pageSize, maxOrder,
                    pageShifts, chunkSize, 0);
        }
        final ByteBuffer memory = allocateDirect(chunkSize
                + directMemoryCacheAlignment);
        return new PoolChunk<ByteBuffer>(this, memory, pageSize,
                maxOrder, pageShifts, chunkSize,
                offsetCacheLine(memory));
    }

    private static ByteBuffer allocateDirect(int capacity) {
        return PlatformDependent.useDirectBufferNoCleaner() ?
                PlatformDependent.allocateDirectNoCleaner(capacity) : ByteBuffer.allocateDirect(capacity);
    }

    @Override
    protected PooledByteBuf<ByteBuffer> newByteBuf(int maxCapacity) {
        if (HAS_UNSAFE) {
            return PooledUnsafeDirectByteBuf.newInstance(maxCapacity);
        } else {
            return PooledDirectByteBuf.newInstance(maxCapacity);
        }
    }

}

三、PoolChunkList

PoolChunkList根据Chunk使用率规格(minUsage、maxUsage)会分为不同的PoolChunkList实例,同一使用率规格的Chunk保存在同一个PoolChunkList实例中,并通过链表的方式存储(head)。Arena维护不同使用率规格Chunk的PoolChunkList,彼此通过PoolChunkList的前(prevList)后(nextList)指针连接。

初始Arena中的所有PoolChunkList的head指针都是空,新建Chunk之后会加入qinit对应的PoolChunkList实例,后续Chunk使用率波动,会在各个PoolChunkList(q000-q100)中来回移动。

成员变量

final class PoolChunkList<T> implements PoolChunkListMetric {
    // 所属Arena
    private final PoolArena<T> arena;
    // 后驱PoolChunkList
    private final PoolChunkList<T> nextList;
    // 前驱PoolChunkList
    private PoolChunkList<T> prevList;
    // Chunk使用率规格下限
    private final int minUsage;
    // Chunk使用率规格上限
    private final int maxUsage;
    // 由当前实例管理的Chunk可分配内存上限(通过minUsage计算得到)
    private final int maxCapacity;
    // Chunk链表头节点 初始为NULL
    private PoolChunk<T> head;
    // 剩余可用内存下限值,小于等于这个值的Chunk需要移动到nextList
    private final int freeMinThreshold;
    // 剩余可用内存上限值,大于这个值的Chunk需要移动到prevList
    private final int freeMaxThreshold;
}

构造方法

PoolChunkList(PoolArena<T> arena, PoolChunkList<T> nextList, int minUsage, int maxUsage, int chunkSize) {
    this.arena = arena;
    this.nextList = nextList;
    this.minUsage = minUsage;
    this.maxUsage = maxUsage;
    // 计算maxCapacity
    maxCapacity = calculateMaxCapacity(minUsage, chunkSize);
    freeMinThreshold = (maxUsage == 100) ? 0 : (int) (chunkSize * (100.0 - maxUsage + 0.99999999) / 100L);
    freeMaxThreshold = (minUsage == 100) ? 0 : (int) (chunkSize * (100.0 - minUsage + 0.99999999) / 100L);
}

这里重点看一下calculateMaxCapacity方法的逻辑。

private static int calculateMaxCapacity(int minUsage, int chunkSize) {
//        minUsage = max(1, minUsage);
	// 1和minUsage取大,保证计算时minUsage>=1
    minUsage = minUsage0(minUsage);
	
    if (minUsage == 100) {
        return 0;
    }
    return  (int) (chunkSize * (100L - minUsage) / 100L);
}

为什么有maxCapacity这个成员变量?考虑Arena操作PoolChunkList分配内存时,PoolChunkList实例需要判断它管理的PoolChunk是否允许分配这么大的内存。比如当前PoolChunkList只能管理20%-50%内存使用率的Chunk,此时客户端需要分配81%*16MB的内存,就算PoolChunkList中有使用率最低为20%的Chunk也无法分配这么大的内存。所以需要计算一个上限值,如果超出这个值的内存分配请求,当前PoolChunkList实例无法分配,返回false。

四、PoolChunk

PoolChunk是一颗完全二叉树,用数组存储(memoryMap)。由Arena创建并保存在PoolChunkList中。

先来看一下成员变量,再来解释上面这张图。

final class PoolChunk<T> implements PoolChunkMetric {
	// 标识当前Chunk属于哪个Arena
    final PoolArena<T> arena;
    // 实际16MB内存块,如果是Direct则为JDKByteBuffer,如果是Heap则为byte数组
    final T memory;
    // 忽略 内存对齐 认为是0
    final int offset;
    // 树,初始值与depthMap一样
    private final byte[] memoryMap;
    // 节点 - 节点深度
    private final byte[] depthMap;
    // 分配Subpage集合(如果Chunk没有分配过小于等于4KB的内存,这里不会有Subpage实例)
    private final PoolSubpage<T>[] subpages;
    // -8192 掩码,用于判断分配内存是否大于8k,x&-8192 != 0 代表超过8k
    private final int subpageOverflowMask;
    // 8192 页大小
    private final int pageSize;
    // log2(页大小) = 13
    private final int pageShifts;
    // 树深度 = 11
    private final int maxOrder;
    // chunk大小 16MB
    private final int chunkSize;
    // log2(chunk大小) = 24
    private final int log2ChunkSize;
    // Subpage数组大小 = 2048 = 16MB/8KB
    private final int maxSubpageAllocs;
    // 标记位 = 树深度 + 1 = 11 + 1 = 12
    private final byte unusable;
	// 缓存ByteBuffer,减少New对象和GC
    private final Deque<ByteBuffer> cachedNioBuffers;
	// 剩余可分配字节数 = 16MB - 已分配字节数
    int freeBytes;
	// 表示目前在哪个PoolChunkList中
    PoolChunkList<T> parent;
    // 前驱节点
    PoolChunk<T> prev;
    // 后驱节点
    PoolChunk<T> next;
}

重点关注下面几个变量:

  • memoryMap:完全树,包含4096个节点(=2^(maxOrder+1)=2^12),实际使用下标1-4095。每个节点有自己的id(即memoryMap的下标)。每个节点的初始值等于节点所处深度,如memoryMap[1]=0、memoryMap[2]=1、memoryMap[2048]=11。节点值大于maxOrder,表示当前节点对应的内存已经分配完毕。memoryMap节点值运行时如何变化,后续再说。

  • depthMap:包含4096个元素,实际使用下标1-4095,下标对应memoryMap下标(节点id),元素值代表下标对应的节点所处深度。整个运行过程中,depthMap是不会变化的。

  • subpages:包含2048个元素(maxSubpageAllocs=1<<maxOrder)。Chunk不仅可以分配大于8K的内存(Page大小),也可以分配Tiny、Small规格的内存。这些小规格内存由Subpage管理,但是由Chunk创建Subpage。如果Chunk从未分配小于等于4K的内存,那么这个数组里不存在元素。

  • maxOrder:树深度,11。

  • unusable:固定值=树深度+1=12,作用于memoryMap保存的值,标识当前节点及其子节点的内存均已被分配,不可用。

  • memory:实际16M内存块,对于直接内存是JDK的ByteBuffer,对于堆内存是byte数组。言外之意,创建完Chunk之后就拥有16M的ByteBuffer,分配不同规格(Normal/Small/Tiny)内存块,只是分配它们在ByteBuffer里占有的区间,即偏移量和长度。可以认为memory是一个原子内存池,管理16M内存资源。

  • freeBytes:剩余可分配字节数。

如上图所示,Chunk中的树共有4095个节点,对应memoryMap的下标1-4095,每个节点的初始值为memoryMap[index]存储的值,即节点所处深度节点所处深度由depthMap数组表示,运行时不会改变

使用Chunk分配内存时,会对用户申请分配的内存大小做一次标准化处理,后续再讨论。 如果分配内存大于等于8K,需要从memoryMap选择空闲节点,标记节点为unusable=12。(大于4K小于8K的都会标准化为8K) 如果分配内存小于等于4K,需要选择memoryMap中空闲的叶子节点,标记为unusable=12,并创建Subpage放入subpages,其中subpages的下标对应memoryMap叶子节点所处位置(看图)。后续由Subpage负责划分更小规格(比如32B等)的内存块供客户端使用。

最后看一下Chunk的构造方法。

PoolChunk(PoolArena<T> arena, T memory, int pageSize, int maxOrder, int pageShifts, int chunkSize, int offset) {
    // 设置一些变量
    unpooled = false;
    this.arena = arena;
    this.memory = memory;
    this.pageSize = pageSize;
    this.pageShifts = pageShifts;
    this.maxOrder = maxOrder;
    this.chunkSize = chunkSize;
    this.offset = offset;
    unusable = (byte) (maxOrder + 1);
    log2ChunkSize = log2(chunkSize);
    subpageOverflowMask = ~(pageSize - 1);
    freeBytes = chunkSize;
    assert maxOrder < 30 : "maxOrder should be < 30, but is: " + maxOrder;
    maxSubpageAllocs = 1 << maxOrder;
    // 创建树
    memoryMap = new byte[maxSubpageAllocs << 1];
    depthMap = new byte[memoryMap.length];
    int memoryMapIndex = 1;
    for (int d = 0; d <= maxOrder; ++ d) {
        int depth = 1 << d;
        for (int p = 0; p < depth; ++ p) {
            memoryMap[memoryMapIndex] = (byte) d;
            depthMap[memoryMapIndex] = (byte) d;
            memoryMapIndex ++;
        }
    }
	// 一个2048个元素的空数组
    subpages = newSubpageArray(maxSubpageAllocs);
    cachedNioBuffers = new ArrayDeque<ByteBuffer>(8);
}

五、PoolSubpage

成员变量

final class PoolSubpage<T> implements PoolSubpageMetric {
    // Subpage所属Chunk
    final PoolChunk<T> chunk;
    // 所处memoryMap下标(树2048-4095节点)
    private final int memoryMapIdx;
    // 分配到ByteBuffer或byte数组的offset
    // 记得整块16M内存块在PoolChunk的memory成员变量中,Subpage只是分得了其中一部分
    // 用这个offset确定当前实例所处偏移量
    private final int runOffset;
    // 页大小 8k
    private final int pageSize;
    // 8长度long数组 最多可以标识64*8=512位二进制位
    // 即8k内存块最小规格=8192/512=16
    private final long[] bitmap;
    // 前驱
    PoolSubpage<T> prev;
    // 后驱
    PoolSubpage<T> next;
    // 当前Subpage管理的内存块规格大小,比如16B、32B
    int elemSize;
    // 页pageSize按照规格elemSize分割得到的内存块上限
    // 如8192/32=256
    private int maxNumElems;
    // 实际使用long位图数组的长度
    // 比如管理16B规格,需要用512位二进制位,需要long数组8个位置
    // 比如管理32B规格,需要用256位二进制位,需要long数组4个位置
    private int bitmapLength;
    // 下一个可分配的位图下标
    private int nextAvail;
    // 剩余可分配内存块数量
    private int numAvail;
}

首先Subpage由一个Chunk创建,Chunk为其分配了memoryMapIdx代表Chunk树的叶子节点,也为其分配了一个runOffset代表当前Subpage处于16MB内存块的偏移量,按照8k的页大小,这个偏移量一定是8192的整数倍。

由于Subpage需要管理各种规格的小内存块,在自己的runOffset之外,还要分配小内存块的偏移量。比如管理32B规格的Subpage分配到了Chunk的2049节点,runOffset=8192,第一个分配的32B内存块在16MB中实际的offset(见PooledByteBuf的offset成员变量)就是8192+0,第二个分配的32B内存块在16MB中实际的offset就是8192+32=8224,依次类推。

bitmap是个long数组,每个long元素可以代表64位二进制位,即可以标识小内存块的32个offset。因为当前Subpage管理32B规格,实际需要8192(页大小)/32(规格)=256个二进制位(maxNumElems),256(二进制位个数)/64(long类型占用字节)= 4,所以bitmap数组只需要使用4个long即可(bitmapLength)。

在运行的过程中,除了首次创建Subpage是从Arena->Chunk->Subpage,创建完成后Subpage会挂在对应Arena的Tiny或Small池中,后续分配同样规格的小内存块会直接走Arena->TinyPool/SmallPool,所以这里有prev和next两个指针,可以把同样规格的Subpage串联起来。

此外随着运行期间Subpage分配小内存块,bitmap慢慢被填充,numAvail剩余可分配内存块数量会减少。当小内存块被归还给Subpage时,bitmap会将标志位恢复为0,并将归还内存块的bitmap位图索引保存到nextAvail供下次分配时直接使用。(后期会发现,小内存块还有ThreadLocal缓存,不一定会直接还给Subpage)

六、PooledByteBuf

PooledByteBuf池化ByteBuf的基类,继承自引用计数ByteBuf抽象类,实现类分为DirectPooledByteBuf和HeapPooledByteBuf,区别是前者使用直接内存底层是JDK的ByteBuffer,后者使用堆内存底层是byte数组。

abstract class PooledByteBuf<T> extends AbstractReferenceCountedByteBuf {
    // 对象池相关(后续讨论)
    private final Handle<PooledByteBuf<T>> recyclerHandle;
    // 属于哪个Chunk
    protected PoolChunk<T> chunk;
    // 低32位是 memoryMap的下标 来源于Chunk
    // 高32位是 bitMap下标(只有subpage分配才有低32位,拆分8k页)来源于Subpage
    protected long handle;
    // 一个大内存块(16M) 对于Direct是JDK的ByteBuffer 对于Heap是byte数组
    protected T memory;
    // 分配到的起始偏移量,针对memory
    protected int offset;
    // 申请内存大小(非标准化)
    protected int length;
    // 实际分配内存大小(标准化)
    // 即申请到16M内存块[offset,offset+maxLength)区间的使用权
    int maxLength;
    // 属于哪个线程缓存(后续讨论)
    PoolThreadCache cache;
    // 分配器
    private ByteBufAllocator allocator;
}

这里重点关注几个变量:

  • chunk:标识当前Buffer属于哪个Chunk。
  • memory:表示当前Buffer属于哪个实际的16MB的内存块。
  • handle:重要变量,持有所属Chunk和Subpage的基因
    • 低32位,标识Chunk树的节点id,即memoryMap的下标。
    • 高32位,对于大于等于8K的内存块,没经过Subpage分配,这32位为0;小于等于4KB的内存块,即Tiny和Small规格内存块,由Subpage分配,这32位记录的是位图下标(bitmap数组,long元素合并后的全局二进制位)。
  • offset、length、maxLength:offset表示当前Buffer位于16MB内存块的起始偏移量,length表示用户原始申请的内存大小,maxLength表示标准化后的内存大小,是实际从16MB里分得的大小。

总结

  • 内存分类

    • 按大小分为:Tiny、Small、Normal、Huge
    • 按结构分为:Chunk、Page
  • PoolArena:持有两个Subpage池和一个PoolChunkList链,用户分配内存的入口就是Arena。负责创建Chunk管理Chunk,管理Subpage。

  • PoolChunkList:PoolChunkList根据Chunk使用率规格会分为不同的PoolChunkList实例,同一使用率规格的Chunk保存在同一个PoolChunkList实例中,并通过链表的方式存储。Arena维护不同使用率规格Chunk的PoolChunkList,彼此通过PoolChunkList的前后指针连接。运行时Arena会根据PoolChunkList设定的阈值,在PoolChunkList链表中互相传递PoolChunk。

  • PoolChunk:Chunk是Netty向系统申请内存的最小单位,大小为16MB。如果是DirectBuffer底层是JDK的ByteBuffer,如果是HeapBuffer底层是byte数组。Chunk是一颗满二叉树,深度为11,节点个数为4095,每个节点代表内存块的一个偏移量,当节点值为12时表示当前节点和其后辈节点的偏移量都不可用。Chunk还负责创建Subpage,将Subpage挂到对应的Arena的Tiny或Small池中。

  • PoolSubpage:PoolSubpage由PoolChunk创建,创建后被放入PoolArena的Tiny或Small池中维护。每个PoolSubpage实际分得8K内存块,但是需要根据自己管理的规格大小,如32B,划分为n个小内存块。小内存块的偏移量由其内部的一个位图维护。