前面一篇主要研究了UnpooledByteBuf
主要工作原理,本文主要探讨 PooledByteBuf
相关。
本文主要从以下几个方面梳理了PooledByteBuf:
PooledByteBuf
构成PoolChunk
组织形式PoolChunk
申请大内存和小内存,以及PoolSubpage
组装构成- 池化内存
PooledByteBuf
分配过程及,分配完后使用原理使用研究。
PooledByteBuf
看看其构造方法:
abstract class PooledByteBuf<T> extends AbstractReferenceCountedByteBuf {
private final Recycler.Handle<PooledByteBuf<T>> recyclerHandle;
protected PoolChunk<T> chunk; // 管理器,表明当前bytebuf属于哪个chunk
protected long handle; // 内存地址
protected T memory; // 内存方式
protected int offset;
protected int length;
int maxLength;
PoolThreadCache cache; // 线程本地缓存
ByteBuffer tmpNioBuf;
private ByteBufAllocator allocator;
...
}
第一眼直接看他构造方法,会比较懵。
PoolChunk
PoolChunk 负责管理和分配内存,里面是以page为单位进行分配。里面以 byte[] memoryMap[]
构造了一颗平衡二叉树来对page进行维护,且可以理解为只有叶子节点代表有效内存块。
上图简明的一个默认的PoolChunk内存管理结构,有以下默认值:
io.netty.allocator.pageSize
默认 pageSize 为 8192io.netty.allocator.maxOrder
默认最大层次是11,即4096个节点组成的完全二叉树
在 PoolChunk
的构造方法中,会初始化这颗二叉树:
for (int d = 0; d <= maxOrder; ++ d) { // move down the tree one level at a time
int depth = 1 << d;
for (int p = 0; p < depth; ++ p) {
// in each level traverse left to right and set value to the depth of subtree
memoryMap[memoryMapIndex] = (byte) d;
depthMap[memoryMapIndex] = (byte) d;
memoryMapIndex ++;
}
}
allocate
从 PooledByteBufAllocator
的 newDirectBuffer
往后看,接下来看PooledChunk
如何分配内存的,跟着源码调用到allocate方法
PoolChunk
的 allocate
方法:
boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
final long handle;
if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
// 大于1页的正常分配
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;
}
分配小内存allocateSubpage(int normCapacity)
netty如何分配内存呢?根据请求内存大小,会执行不同策略,如果请求>= 512
,则按正常page调出。否则会将page进一步拆解,进行更细粒度内存分配。即将一个page,划分为多个连串的 PoolSubpage
数组
PoolChunk
的 allocateSubpage
方法:
private long allocateSubpage(int normCapacity) {
// 根据容量,获取一个PoolSubpage 的头结点
PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
int d = maxOrder; // subpages are only be allocated from pages i.e., leaves
synchronized (head) {
int id = allocateNode(d);
if (id < 0) {
return id;
}
final PoolSubpage<T>[] subpages = this.subpages;
final int pageSize = this.pageSize;
freeBytes -= pageSize;
// 获取当前的subpages
int subpageIdx = subpageIdx(id);
PoolSubpage<T> subpage = subpages[subpageIdx];
if (subpage == null) {
subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
subpages[subpageIdx] = subpage;
} else {
subpage.init(head, normCapacity);
}
return subpage.allocate();
}
}
- 首先根据规整后容量,从PoolArea中获取一个对应规格下的PoolSubpage的头结点。
- 加锁,进行分配,直接根据tree的高度,获取一个叶子节点。
- 维护freeBytes,即哪些内存还未分配。
- 根据找到的page下标,尝试去subpages中找,subpages默认为2048,即对应page数量。
- 初始化之后,尝试给分配内存。
上面代码有几个过程单独拎出来研究。
findSubpagePoolHead
findSubpagePoolHead
方法主要根据 分配大小搜索到这类大小对应的PoolSubpage头结点:
PoolSubpage<T> findSubpagePoolHead(int elemSize) {
int tableIdx;
PoolSubpage<T>[] table;
if (isTiny(elemSize)) { // < 512
tableIdx = elemSize >>> 4;
table = tinySubpagePools;
} else {
tableIdx = 0;
elemSize >>>= 10;
while (elemSize != 0) {
elemSize >>>= 1;
tableIdx ++;
}
table = smallSubpagePools;
}
return table[tableIdx];
}
即根据 elemSize
取模16后即为 tinySubpagePools
下标位置。tinySubpagePools
默认大小为32。
为什么是32,且为什么要除16呢?
因为小于512b的申请量为tiny size,而32*16=512,所以netty根据tiny size 大小,又构造出一个数组+PoolSubpage链表结构。
而 small size 的 smallSubpagePools
大小为 pageShift-9
,即默认为4个,即512,1024,2048,4096大小。
allocateNode
分配结点,因为无论如何,找到一个subpage之后,也需要去由page来分配,传入d,则说明获取一个叶子节点,因为本身就是分配小内存,所以一个叶子节点够用。
private int allocateNode(int d) {
int id = 1;
int initial = - (1 << d); // has last d bits = 0 and rest all = 1
byte val = value(id);
if (val > d) { // unusable 不可用
return -1;
}
while (val < d || (id & initial) == 0) { // id & initial == 1 << d for all ids at depth d, for < d it is 0
id <<= 1;
val = value(id); // 一层一层遍历
if (val > d) {
id ^= 1;
val = value(id);
}
}
byte value = value(id);
assert value == d && (id & initial) == 1 << d : String.format("val = %d, id & initial = %d, d = %d",
value, id & initial, d);
setValue(id, unusable); // mark as unusable 因为已经被分出去了,设置其为不可用
updateParentsAlloc(id); // 一层一层更新父节点可用状态
return id;
}
- 从第一层开始,找到第d层,再找其可用的id
- 设置id状态为unusable,即maxOrder+1.
- 调用
updateParentsAlloc
将各级祖先节点更新可用状态值。
前面知道netty管理内存池,是通过一颗完全二叉树来进行的,默认是11层,即总共大小为2048*8096=16777216(2^24) b。那么如何通过树来管理的呢?
depth = 0 时候,1个node,每个page大小为chunkSize
depth = 1 时候,2个node,每个page大小为chunkSize/2
…
depth = d 时候,2^d
nodes,每个page大小为chunkSize/2^d
对于分配时候,如果一个节点page大小为8096,有以下情况:
- 如果如果此时申请1b,那memoryMap[2048] = 12 (总层数+1,)说明不能分配,分配完之后,其每一层父节点一直到跟节点值,都要阶梯+1,表示其最大可分配数
- 如果要申请8096b,则memoryMap[1024] =12,说明不能分配,同理父节点一直到更节点,将网上变更值。
总结有以下情况:
- memoryMap[id] = depth_of_id => 整个节点控制的所有叶子结点都是可分配的
- memoryMap[id] > depth_of_id => 说明其管理的最少一个page被分配,所以不能分配他,但是他的一些儿子节点能够被分配
- memoryMap[id] = maxOrder + 1 => 说明其不可分配,其所有孩子节点也无法分配,并且会被标记为unusable。
PoolSubpage
直接看 PoolSubpage
构造方法,new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
,前面知道了normCapacity范围,tiny size 从16到512 不等。所以normaCapacity
在 PoolSubpage
就是叫 elemSize
。
PoolSubpage(PoolSubpage<T> head, PoolChunk<T> chunk, int memoryMapIdx, int runOffset, int pageSize, int elemSize) {
this.chunk = chunk;
this.memoryMapIdx = memoryMapIdx;
this.runOffset = runOffset;
this.pageSize = pageSize;
bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64
init(head, elemSize);
}
初始化大小位置,PoolSubpage 中分配内存,每一块并没有用一个数组进行,而是使用 nextAvail
数组指针来分配。
void init(PoolSubpage<T> head, int elemSize) {
doNotDestroy = true;
this.elemSize = elemSize;
if (elemSize != 0) {
maxNumElems = numAvail = pageSize / elemSize;
nextAvail = 0;
bitmapLength = maxNumElems >>> 6;
if ((maxNumElems & 63) != 0) {
bitmapLength ++;
}
for (int i = 0; i < bitmapLength; i ++) {
bitmap[i] = 0;
}
}
// 将当前PoolSubpage 加到head后面
addToPool(head);
}
- 使用bitmap 字段的每一位来标识数组每一块使用情况,例如 bitmap大小为8位,long 类型占用64位,当elemSize=16时候,需要 8096/16= 506个位置来标识每一个elem使用情况,8个long类型总共有64*8=512位,能够标识每一个elem使用情况。
- 将当前PoolSubpage 加到head 为首链表后面。
PoolSubpage的allocate
该方法原理为返回bitmap的下标返回。
long allocate() {
if (elemSize == 0) {
return toHandle(0);
}
if (numAvail == 0 || !doNotDestroy) {
return -1;
}
final int bitmapIdx = getNextAvail();
int q = bitmapIdx >>> 6;
int r = bitmapIdx & 63;
assert (bitmap[q] >>> r & 1) == 0;
bitmap[q] |= 1L << r;
if (-- numAvail == 0) {
removeFromPool();
}
return toHandle(bitmapIdx);
}
allocateRun(normCapacity) 分配正常容量
PoolChunk
的 allocateRun
方法:
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;
}
- 根据传入的大小,来计算需要分配层数d
- 根据d仍然调用
allocateNode
分配。 - 更新freeBytes数量,返回id。
有细心同学可能回想,如果计算出来的d,从d层上找到的id如果不能分配了咋办?
当然这个不用担心,netty在 PoolArena 中 构造了不同PoolChunkList 的使用量,会从大到小进行选择分配,能进入allocateRun方法,在前一步已经判断过能够在当前PoolChunk中分配了。
如果最终无法分配足够内存,则返回false,而外层 allocateNormal
则会在进行 asser success
则会断言失败。
分配内存
前面都是谈如何管理内存,但是好像还没有设计到更加的底层实现,即我想看到要么就是byte[],要么就是ByteBuffer关联。
PoolChunk<T>
是一个泛型类。其内部通过 PoolArena<T>
来统一管理,PoolArena<T>
有两种内部实现:
HeapArena
传入的数据为泛型数组:
@Override
protected PoolChunk<byte[]> newChunk(int pageSize, int maxOrder, int pageShifts, int chunkSize) {
return new PoolChunk<byte[]>(this, newByteArray(chunkSize), pageSize, maxOrder, pageShifts, chunkSize, 0);
}
DirectArena
传入的为 ByteBuffer
:
@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));
}
directMemoryCacheAlignment
则为直接内存的偏移量。
PooledByteBuf中使用内存
以 PooledUnsafeDirectByteBuf
为例,其 setBytes
方法为:
@Override
protected void _setByte(int index, int value) {
UnsafeByteBufUtil.setByte(addr(index), (byte) value);
}
addr(index)
由逻辑地址转化为池中申请的内存的物理地址下标,申请的单位为PoolChunk。- 将value 调用jni 方法put到对应内存地址处。
总结
本文主要从以下几个方面梳理了PooledByteBuf:
PooledByteBuf
构成。PoolChunk
组织形式。PoolChunk
申请大内存和小内存,以及PoolSubpage
组装构成。- 池化内存
PooledByteBuf
分配过程及,分配完后使用原理使用研究。
关注博主公众号: 六点A君。
哈哈哈,一起研究Netty: