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具体用处。一步步探索吧。