吃透Netty源码系列二十三之池化内存分配四
PoolSubpage的allocate
继续上一篇的,现在执行到subpage.allocate()
,里面才是对子页可分配的内存做了记录并分配了位图的一位,并返回一个64
位的句柄handle
,前面说要解释这个句柄到底做什么的,现在就开始吧。
老规矩,看源码,我来解释:
long allocate() {
if (elemSize == 0) {
return toHandle(0);//一般不会申请0内存,为什么这里还要处理,暂时没弄明白
}
if (numAvail == 0 || !doNotDestroy) {//没有可用的容量或者要销毁
return -1;
}
//获取下一个可用的位图索引,不是位图数组
final int bitmapIdx = getNextAvail();
int q = bitmapIdx >>> 6;//获取所在的位图在数组中的索引
int r = bitmapIdx & 63;//获取64余数,0-63 位图中的索引信息 也就是第几位要设置为1
assert (bitmap[q] >>> r & 1) == 0;//根据位图索引获取这个位图中的位置是0表示可用
bitmap[q] |= 1L << r;//将可用的位图索引设置为1,即不可用
if (-- numAvail == 0) {//如果没有可用了,就从链表中删除
removeFromPool();
}
return toHandle(bitmapIdx);
}
getNextAvail
这个就是获取下一个位图的索引bitmapIdx
,这里的索引是32
位是由两部分组成的:
这个是怎么算的,来看看具体源码吧:
this.nextAvail
最开始是0
,会直接返回0
,并且把this.nextAvail
设置为-1
,这样第二次就要进行findNextAvail
了,这样确实第一次就不需要运算了,用一个判断来提升性能。
private int getNextAvail() {
int nextAvail = this.nextAvail;
if (nextAvail >= 0) {
this.nextAvail = -1;
return nextAvail;//第一次直接返回,后面就要findNextAvail
}
return findNextAvail();//-1表示要找
}
findNextAvail
bitmapLength
就是实际上用到的位图数,所以不需要整个位图数据来遍历,只需要遍历bitmapLength
个位图。
把每个位图取出来按位取反,如果不为0
,说明没取反前还有位置有0
存在,这个位图还能用,否则就说明所有位都是1
了,取反就等于0
,不可用了,如果还能用,就要看用哪一位findNextAvail0
,默认是从低位开始的。如果都不能用就返回-1
。
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;
}
findNextAvail0
这里是真正使用位图的时候,先看传入参数,传入的i
其实就是位图所在位图数组的索引,bits
就是位图本身。首先会有i << 6
,这个就是先求出bitmapIdx
的26
位高位的值baseVal
,就是位图索引对应的值,然后遍历位图,j
表示的就是位图里的第j
位,也就是bitmapIdx
的低6
位,而且是从低位开始判断,取值是0-63
。然后(bits & 1)
取出最低位,如果是0
,表示可用分配,然后就将baseVal | j
,表示高26
位和低6
位合并了,这样才算是完整的bitmapIdx
。而且这样计算出来的bitmapIdx
刚好也是已经分配的内存数-1
,所以最后还要和最大可分配数比较,小于才能分配,因为等于就已经超出最大可分配数maxNumElems
,因为bitmapIdx
是从0
开始的,最大应该是maxNumElems -1
。如果低位不可分配就将位图右移一位,也就是取出第二位继续判断。
private int findNextAvail0(int i, long bits) {
final int maxNumElems = this.maxNumElems;//最大可分配数
final int baseVal = i << 6;//要分配的起始索引,根据第i个位图,如果是0表示0-63进行分配 1表示64-127分配
//遍历位图的每一位,从最低位开始遍历,0表示没有过
for (int j = 0; j < 64; j ++) {//j表示位图里第j位,从低位到高位0-63
if ((bits & 1) == 0) {//取出最低位,为0表示可用
int val = baseVal | j;//加上位置序号j后的新的索引值,比如开始是0,第一个+0索引就是0,然后+1,索引1,类似最后索引是baseVal+63
if (val < maxNumElems) {//如果索引没到最大可分配数就返回,其实最大索引就是maxNumElems-1
return val;
} else {
break;//等于就不行了,跳出循环
}
}
bits >>>= 1;//位图右移,即从低位往高位
}
return -1;
}
举个例子,比如我申请的内存是32B
,那么总共需要的位图应该是8k/32/64=4
个,最大可分配数maxNumElems=8k/32=256
,这里的这4
个位图,每一个刚好对应一个内存分配,总共256
个。现在32B
的内存来了,首先拿出第0
个位图,也就是i=0
,发现可用,于是就开始0<<<6=0
,高26
位全是0
了,再看位图的64位情况,发现底0位就可以用,与是就返回了,这个bitmapIdx
就是0
。同样的,假设第0
个位图用满了,如果我又来了一个32B
,那就需要用第1
个位图,最后返回的bitmapIdx
就是1000000
。
我们继续,看看返回后做了什么。
首先是bitmapIdx >>> 6
,得到高26
位,获取位图在位图数组的索引。
然后bitmapIdx & 63
,得到低6
位,,获取位图内部索引。
最后进行设置bitmap[q] |= 1L << r
,将这个位图内部索引所对应的值设置为1
,表示不可用了。
用我上面举的例子,画了一个bitmapIdx
的变化图:
removeFromPool
如果发现可用的内存数量numAvail
是0
了,就会将这个页从双向循环链表中删除。
private void removeFromPool() {
assert prev != null && next != null;
prev.next = next;
next.prev = prev;
next = null;
prev = null;
}
toHandle重点以及为什么要加上0x4000000000000000L
private long toHandle(int bitmapIdx) {//
return 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;
}
这个也是关键点,首先他会把bitmapIdx
结合内存映射索引memoryMapIdx
一起转化为64
位数。
(long) bitmapIdx << 32 | memoryMapIdx
低32
位是内存映射索引memoryMapIdx
,高32
中的低6
位是所在位图的内部索引,占据64
位的位图中的0-63
位的某一位,其余26
位就是位图所在位图数组的索引。
而最后加上0x4000000000000000L
,开始没想明白,后来发现了,因为不想让高32
位是0
,如果是第一次分配,bitmapIdx
是0,memoryMapIdx=2048
,高32
位就是0
,返回就是2048
,这样不会被当做子页分配来处理。我们看下面的方法:
可以看到allocateRun
是分配8k
以及以上的内存,可以直接返回内存映射索引的,也可以是2048
,而allocateSubpage
通过toHandle
方法,如果没有加上0x4000000000000000L
,第一次分配bitmapIdx=0
,返回的也是2048
。这样在后面的initBuf
方法就会出问题:
上面的方法中,对于2048
来说,取出的bitmapIdx
都是0
,因此子页分配的就不会调用initBufWithSubpage
,会发现内部的chunk
偏移是不对的,少了子页的内部偏移,就会出问题。
所以为了识别子页的bitmapIdx
,最高非符号位设置了1
。这里可能还有问题,如果bitmapIdx
高26
位里面,刚好跟他或起来进位了,又变成0
了,这个不用担心,第一bitmapIdx
高26
位前面说了,是位图数组的索引,初始化的时候对于最小尺寸16B
已经创建好了,数组长度就是8
,而且最高位符号位也不可能是1
呀,是正数呀,不会进位变成0
。
好了,今天就到这里了,希望对学习理解有帮助,大神看见勿喷,仅为自己的学习理解,能力有限,请多包涵。