PoolChunk负责内存的分配与释放,其内部最小的分配单元为page,page的默认大小为8k。如果我们申请很多小块内存时,都按照page来分配,那么资源浪费可不是一点半点。针对这个问题,netty将page拆成了更小的内存块element,但超出了PoolChunk的负责范围,此时netty使用PoolSubpage来解决这个问题。
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);
}
无非是赋值,我们来看下bitmap数组大小为什么是pageSize>>10;从注释中可以看出,16B是elementSize最小值,64是long的位数,也就是说用bitmap的位个数可以满足分配最多element时的数目。bitmap大小为什么不使用pageSize/elemenSize/64?因为申请最大空间后,对于后面可回收后重新分配该对象,不需要重新开辟空间,只需重新调用init()赋值。
head是之前调用arena.findSubpagePoolHead(normCapacity)的返回值。可以从函数调用中分析出,该方法要不是在构造函数中调用,或者在subpage回收后重新分配时调用。
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;
}
}
addToPool(head);
}
先把标志位doNoDestroy设为true,表示该page在使用中,不能被清除。然后赋值。计算出实际使用的bitmapLength大小。并把标志位清零(因为之前可能用过有垃圾数据,仅需把当前要用的部分清零)。
chunk在分配空间时,大小8k以下的空间交给subPage管理,然而chunk并为将subPage暴露给外面,于是subPage通过addToPool()方法,将自己加入到chunk.arean的pool中。
private void addToPool(PoolSubpage<T> head) {
assert prev == null && next == null;
prev = head;
next = head.next;
next.prev = this;
head.next = this;
}
仅仅是链表插入操作,将当前节点插入到head之后。
回到chunk中分配小于8k以下部分的方法中
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();
我们来看下subpage.allocate();
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);
}
如果此时没有可分配的element,那么直接返回。getNetxAvail()无非找到当前page中可分配的段的下标,然后在bitmap上标记该段被使用。如果该page所有可用element分配完了后,将这个subpage从pool中删除。
我们可以看到这里在subPage这个数据结构上进行“管理分配”,返回一个element的index。toHandle操作无非在返回的long上标记可用空间。
private long toHandle(int bitmapIdx) {
return 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;
}
一个long,其中低32位用来表示memoryMapIdx,无非是执行分配的具体page,高32位中的低6位表示bitmap,即定位到当前page中可分配的段。
我们来看下getNextAvail方法。
private int getNextAvail() {
int nextAvail = this.nextAvail;
if (nextAvail >= 0) {
this.nextAvail = -1;
return nextAvail;
}
return findNextAvail();
}
直接从成员中取出下一个可用的位置,如果nextAvail>=0,要么是刚初始化,或者是有element重新回收未分配的,直接返回,并把其标记为-1,否则继续调用findNextAvail。
private int findNextAvail() {
final long[] bitmap = this.bitmap;
final int bitmapLength = this.bitmapLength;
for (int i = 0; i < bitmapLength; i ++) {
long bits = bitmap[i];
if (~bits != 0) {
return findNextAvail0(i, bits);
}
}
return -1;
}
没有明确指定nextAvail位置时,于是从头到尾查。先定位到bitmaps中的哪个long没有分配完。
private int findNextAvail0(int i, long bits) {
final int maxNumElems = this.maxNumElems;
final int baseVal = i << 6;
for (int j = 0; j < 64; j ++) {
if ((bits & 1) == 0) {
int val = baseVal | j;
if (val < maxNumElems) {
return val;
} else {
break;
}
}
bits >>>= 1;
}
return -1;
}
于是找那个long上的哪个位未分配,于是构造val返回。val就是最低6位表示该long上的哪个字节,其他位即val >> 6后记录的是哪个long,即bitmap的下标。其实val就是bitmapIdx,它定位到具体的某个element。
关于上一篇遗留问题,总结一下。
poolChunk的allocate函数返回一个long类型的handle,其中当handle<Integer.MAX_VALUE时,它表示chunk的节点id,当handle>Integer.MAX_VALUE,他分配的是一个Subpage,节点id=memoryMapIdx, 且能得到Subpage的bitmapIdx。
再来看下poolChunk对应的小于8k的free过程。
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;
}
}
}
先根据handle取到bitmapIdx,再取出对应的subpage,找到根据elemSize取出head,加锁(应为涉及到head为头的链表的改变),再调用subpage.free。
boolean free(PoolSubpage<T> head, int bitmapIdx) {
if (elemSize == 0) {
return true;
}
int q = bitmapIdx >>> 6;
int r = bitmapIdx & 63;
assert (bitmap[q] >>> r & 1) != 0;
bitmap[q] ^= 1L << r;
setNextAvail(bitmapIdx);
if (numAvail ++ == 0) {
addToPool(head);
return true;
}
if (numAvail != maxNumElems) {
return true;
} else {
// Subpage not in use (numAvail == maxNumElems)
if (prev == next) {
// Do not remove if this subpage is the only one left in the pool.
return true;
}
// Remove this subpage from the pool if there are other subpages left in the pool.
doNotDestroy = false;
removeFromPool();
return false;
}
}
无非在bitmap上标记为1可用,然后将当前bitmapIdx设置到nextAvail,如果当前subpage的可用大小为0,那么把可用大小加1,并且添加到pool中,即head为头的双向链表中。如果添加后numAvail==maxNumElems说明该page上所有element都未分配。如果该subpage是链上的唯一一块那么不处理(这样做尽可能的保证arena分配小内存时能直接从pool中取,而不用再到chunk中去获取),否则将其从arean的pool的链表中剔除。连着写了两篇,netty内存池的结构越来越清晰了,当然问题还有不少,比如分配大小大于一整个chunk后如何处理,subpage的elemSize存在很多可能值,handle具体用处。一步步探索吧。