Netty源码分析(七) PoolChunk
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Netty源码分析(七) PoolChunk相关的知识,希望对你有一定的参考价值。
参考技术A 在分析源码之前,我们先来了解一下Netty的内存管理机制。我们知道,jvm是自动管理内存的,这带来了一些好处,在分配内存的时候可以方便管理,也带来了一些问题。jvm每次分配内存的时候,都是先要去堆上申请内存空间进行分配,这就带来了很大的性能上的开销。当然,也可以使用堆外内存,Netty就用了堆外内存,但是内存的申请和释放,依然需要性能的开销。所以Netty实现了内存池来管理内存的申请和使用,提高了内存使用的效率。PoolChunk就是Netty的内存管理的一种实现。Netty一次向系统申请16M的连续内存空间,这块内存通过PoolChunk对象包装,为了更细粒度的管理它,进一步的把这16M内存分成了2048个页(pageSize=8k)。页作为Netty内存管理的最基本的单位 ,所有的内存分配首先必须申请一块空闲页。Ps: 这里可能有一个疑问,如果申请1Byte的空间就分配一个页是不是太浪费空间,在Netty中Page还会被细化用于专门处理小于4096Byte的空间申请 那么这些Page需要通过某种数据结构跟算法管理起来。
先来看看PoolChunk有哪些属性
Netty采用完全二叉树进行管理,树中每个叶子节点表示一个Page,即树高为12,中间节点表示页节点的持有者。有了上面的数据结构,那么页的申请跟释放就非常简单了,只需要从根节点一路遍历找到可用的节点即可。主要来看看PoolChunk是怎么分配内存的。
Netty的内存按大小分为tiny,small,normal,而类型上可以分为PoolChunk,PoolSubpage,小于4096大小的内存就被分成PoolSubpage。Netty就是这样实现了对内存的管理。
PoolChunk就分析到这里了。
Netty内存池之PoolChunk
netty4之后,netty中加入了内存池管理,看了netty后突然对这块很感兴趣,怎么办?最简单的方式无非看源码呗。我们顺着思路强行分析一波。分析下简单需求,为了能够简单的操作内存,必须保证每次分配到的内存时连续的。我们来看下netty的poolChunk。
PoolChunk
该类主要负责内存块的分配与回收,我们可以从源码中给的注释差不多看清PoolChunk的数据结构与算法。先介绍两个术语:
page-是可以分配的内存块的最小单位
chunk-是一堆page的集合
下面是chunk的构造方法
PoolChunk(PoolArena<T> arena, T memory, int pageSize, int maxOrder, int pageShifts, int chunkSize, int offset) {
unpooled = false;
this.arena = arena;
// memory是一个容量为chunkSize的byte[](heap方式)或ByteBuffer(direct方式)
this.memory = memory;
// 每个page的大小,默认为8192,8k
this.pageSize = pageSize;
// 13
this.pageShifts = pageShifts;
// 11层
this.maxOrder = maxOrder;
// 8k * 2048
this.chunkSize = chunkSize;
this.offset = offset;
unusable = (byte) (maxOrder + 1);
log2ChunkSize = log2(chunkSize);
// -8192
subpageOverflowMask = ~(pageSize - 1);
freeBytes = chunkSize;
assert maxOrder < 30 : "maxOrder should be < 30, but is: " + maxOrder;
maxSubpageAllocs = 1 << maxOrder;
// Generate the memory map.
memoryMap = new byte[maxSubpageAllocs << 1];
depthMap = new byte[memoryMap.length];
int memoryMapIndex = 1;
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 ++;
}
}
subpages = newSubpageArray(maxSubpageAllocs);
}
可能光看构造我们会摸不清头脑,我去网上挖了张图,方便大家理解。
Netty中PoolChunk内部维护一棵满平衡二叉树memoryMap,所有子节点管理的内存也属于其父节点。这么理解,chunk是真正分配内存的空间,它里面的page就是最小可分配的单元,我们发现平衡二叉树的叶子节点(11层)与page一一对应,再往上走,比如1024节点,它指向2048跟2049,那无非1024节点指向了page0跟page1两个存储单元。我们继续看下图
这图跟上图的区别无非把节点的id换成了层数。其实完全二叉树天生就可以用数组来表示,于是id跟层数对应起来可以用数组memoryMap表示。数组下标为id,值为层数。depthMap也是这样存的,但它初始化后数据不变了,仅仅查表作用。poolChunk维护memoryMap主要用来标记使用情况。
1) memoryMap[id] = depth_of_id => it is free / unallocated
* 2) memoryMap[id] > depth_of_id => at least one of its child nodes is allocated, so we cannot allocate it, but
* some of its children can still be allocated based on their availability
* 3) memoryMap[id] = maxOrder + 1 => the node is fully allocated & thus none of its children can be allocated, it
* is thus marked as unusable
顺便举个例子吧,我们看下id为512的节点,它层数为9,此时memoryMap[512]=9,说明512节点下面的page一个都没分配,如果此时我们分配了一块page1(随便抽的),那么按照算法,memoryMap[2049]=12,memoryMap[1024]=11,memoryMap[512]=10。一直更新上去。后面会具体从代码层面分析。
道理讲了这么多,看代码吧。构造函数之前都是简单赋值,大小我也注释都标上了,我们看下下面这块代码,其实逻辑也很简单,无非初始化memoryMap跟depthMap,此时可以注意到memoryMap的第一个节点即memoryMap[0]=0,这个元素是空出来的,下标为1的元素才是代表第一个节点,这样也有好处,下标为i的父元素的左孩子就是2*i(i<<1),右孩子呢? 2*i+1 ((i<<1)^1)。位运算效率总是讨人喜欢的。
memoryMap = new byte[maxSubpageAllocs << 1];
depthMap = new byte[memoryMap.length];
int memoryMapIndex = 1;
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 ++;
}
}
subpages = newSubpageArray(maxSubpageAllocs);
最后无非赋初值subpages。
poolChunk的数据结构差不多说清楚了,具体来看下如何向PoolChunk申请空间的
allocate()函数
long allocate(int normCapacity) {
if ((normCapacity & subpageOverflowMask) != 0) {
return allocateRun(normCapacity);
} else {
return allocateSubpage(normCapacity);
}
}
一开始的位运算就用的很讨人喜欢,(normCapacity & subpageOverflowMask) != 0,normCapacity无非是申请空间的字节数大小,我们可以从构造函数中看到 subpageOverflowMask = ~(pageSize - 1); 那么subpageOverflowMask大小为-8192(光看数值看不出所以然,它在内存中是最高位跟低13位全为0,剩下全为1)那么很容易想到,这句话意思无非等价于normCapacity >= pageSize;
然后分两种情况,当申请大小大于pageSize,那么一块page肯定不够,于是需要多块,返回的是可分配normCapacity大小的节点的id;第二中情况,一个page已经够分了,但是实际应用中会存在很多小块内存的分配,如果小块内存也占用一个page明显浪费,于是page也可以继续拆分了,这是后话。我们先来看下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表示申请normCapacity大小对应的节点的深度,通过很简单的运算即(normCapacity/8k)再取对数,拿maxOrder(11)减一下。
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表示申请normCapacity大小对应的节点的深度,通过很简单的运算即(normCapacity/8k)再取对数,拿maxOrder(11)减一下。
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;
}
id赋初值为1,从根节点开始找,如果(val > d)第一层的节点的值大于d,表示d层及以上都不可分配,说明当前Chunk取不出那么大的空间,此次分配失败。一直在树上找直到找到与d恰好匹配的节点,即高度不小于d并且最小可分配,id<<=1往下一层走;当前节点对应大小不够分配,则id^=1切换到父节点的另一子节点中(若父节点够分配,则此节点肯定能分配);找到对应节点id后,把当前节点的值设为不可用,并且递归循环调整父节点的MemoryMap的值
private void updateParentsAlloc(int id) {
while (id > 1) {
int parentId = id >>> 1;
byte val1 = value(id);
byte val2 = value(id ^ 1);
byte val = val1 < val2 ? val1 : val2;
setValue(parentId, val);
id = parentId;
}
}
代码逻辑很简单,父节点值设为子节点中的最小值。直到根节点,调整结束。
我们再看当申请大小小于pageSize的第二种情况:
private long allocateSubpage(int normCapacity) {
// Obtain the head of the PoolSubPage pool that is owned by the PoolArena and synchronize on it.
// This is need as we may add it back and so alter the linked-list structure.
PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
synchronized (head) {
int d = maxOrder; // subpages are only be allocated from pages i.e., leaves
int id = allocateNode(d);
if (id < 0) {
return id;
}
final PoolSubpage<T>[] subpages = this.subpages;
final int pageSize = this.pageSize;
freeBytes -= pageSize;
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();
}
}
针对这种情况,可以将8K的page拆成更小的块,这已经超出chunk的管理范围了,这个时候就出现了PoolSubpage, 通过normCapacity大小找到对应的PoolSubpage头结点,然后加锁(分配过程会改变链表结构),d=maxOrder,因此此时只需一块page足够,所以d直接设置为maxOrder即叶子节点就ok,通过allcNode()分配到节点的id,如果id小于0表示分配失败(叶子节点分配完了)返回。之后找到最高的合适节点后,开始新建(并加入到poolChunk的subpages数组中)或初始化subpage,然后调用subpage的allocate()申请空间,返回了个handle;下一篇博文会将subpage的具体逻辑。
我们再来看下释放空间,free()函数。
void free(long handle) {
int memoryMapIdx = memoryMapIdx(handle);
int bitmapIdx = bitmapIdx(handle);
if (bitmapIdx != 0) { // free a subpage
PoolSubpage<T> subpage = subpages[subpageIdx(memoryMapIdx)];
assert subpage != null && subpage.doNotDestroy;
// Obtain the head of the PoolSubPage pool that is owned by the PoolArena and synchronize on it.
// This is need as we may add it back and so alter the linked-list structure.
PoolSubpage<T> head = arena.findSubpagePoolHead(subpage.elemSize);
synchronized (head) {
if (subpage.free(head, bitmapIdx & 0x3FFFFFFF)) {
return;
}
}
}
freeBytes += runLength(memoryMapIdx);
setValue(memoryMapIdx, depth(memoryMapIdx));
updateParentsFree(memoryMapIdx);
}
其中传入的handle无非是allocate得到的handle,通过handle得到page的id,如果通过handle计算得到的bitmapIdx为0,则表示是之前申请空间的第一种条件,大小大于pageSize,于是直接释放整个节点对应的page空间,则将其memoryMap[id]=其对应的层数(depth[id]),于是调整父亲节点,此时是申请时的逆过程。
private void updateParentsFree(int id) {
int logChild = depth(id) + 1;
while (id > 1) {
int parentId = id >>> 1;
byte val1 = value(id);
byte val2 = value(id ^ 1);
logChild -= 1; // in first iteration equals log, subsequently reduce 1 from logChild as we traverse up
if (val1 == logChild && val2 == logChild) {
setValue(parentId, (byte) (logChild - 1));
} else {
byte val = val1 < val2 ? val1 : val2;
setValue(parentId, val);
}
id = parentId;
}
}
如果左右孩子value值一样,则父节点value设为孩子的value-1,否则设为孩子节点的最小值。
第二种情况,则需释放subpage再继续上面介绍的释放page。
if (bitmapIdx != 0) { // free a subpage
PoolSubpage<T> subpage = subpages[subpageIdx(memoryMapIdx)];
assert subpage != null && subpage.doNotDestroy;
// Obtain the head of the PoolSubPage pool that is owned by the PoolArena and synchronize on it.
// This is need as we may add it back and so alter the linked-list structure.
PoolSubpage<T> head = arena.findSubpagePoolHead(subpage.elemSize);
synchronized (head) {
if (subpage.free(head, bitmapIdx & 0x3FFFFFFF)) {
return;
}
}
}
这儿有涉及到subpage部分不是讲得很清,没关系,以poolChunk为netty内存池的切入点。接下来会,向下细挖subpage,向上探索poolArea等等。
以上是关于Netty源码分析(七) PoolChunk的主要内容,如果未能解决你的问题,请参考以下文章