前言
本章学习Netty内存池:
- 内存分类
- PoolArena
- PoolChunkList
- PoolChunk
- PoolSubpage
一、内存分类
1、按照大小分类
| 分类 | 下限 | 上限 | 规格 |
|---|---|---|---|
| Tiny | >0Byte | <=496Byte | 16B、32B...496B,差值为16的等差数列,共31种规格 |
| Small | >=512Byte | <=4096Byte | 512B、1024B、2048B、4096B,比值为2的等比数列,共4种规格 |
| Normal | >=8192Byte | <=16MB | 8192B |
| 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的规格如下:
| 分类 | 下限 | 上限 |
|---|---|---|
| qInit | Integer.MIN_VALUE | 25% |
| q000 | 1% | 50% |
| q025 | 25% | 75% |
| q050 | 50% | 100% |
| q075 | 75% | 100% |
| q100 | 100% | 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个小内存块。小内存块的偏移量由其内部的一个位图维护。