Netty源码分析:PoolSubpage
在上篇介绍Netty源码分析:PoolChunk的博文中,我们分析了allocateSubpage方法(如下)的前半部分,后半部分是借助于PoolSubpage来完成的。这篇博文就介绍下PoolSubpage这个类。
private long allocateSubpage(int normCapacity) {
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>(this, id, runOffset(id), pageSize, normCapacity);
subpages[subpageIdx] = subpage;
} else {
subpage.init(normCapacity);
}
return subpage.allocate();
}
1、PoolSubpage类的属性和构造函数
final class PoolSubpage<T> {
final PoolChunk<T> chunk;//用来表示该Page属于哪个Chunk
private final int memoryMapIdx;//用来表示该Page在Chunk.memoryMap中的索引
// 当前Page在chunk.memoryMap的偏移量
private final int runOffset;
private final int pageSize;//Page的大小,默认为8192
/*
long 类型的数组bitmap用来表示Page中存储区域的使用状态,
数组中每个long的每一位表示一个块存储区域的占用情况:0表示未占用,1表示占用。
例如:对于一个4K的Page来说如果这个Page用来分配1K的存储与区,
那么long数组中就只有一个long类型的元素且这个数值的低4危用来指示4个存储区域的占用情况。
*/
private final long[] bitmap;
//PoolSubpage本身设计为一个链表结构
PoolSubpage<T> prev;
PoolSubpage<T> next;
boolean doNotDestroy;
int elemSize;//块的大小
private int maxNumElems;//page按elemSize大小分得的块个数
private int bitmapLength;//bitmap数组实际会用到的长度,等于pageSize/elemSize/64
// 下一个可用的位置
private int nextAvail;
// 可用的段数量
private int numAvail;
PoolSubpage(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,这里的16指的是块的最小值,64是long类型的所占的bit数。
init(elemSize);
}
如在Netty源码分析:PoolChunk的博文中介绍的:
对于小于一个Page的内存,Netty在Page中完成分配。每个Page会被切分成大小相同的多个存储块,存储块的大小由第一次申请的内存块大小决定。对于Page的大小为4K,第一次申请的时1K,则这个Page就会被分成4个存储块。
一个Page只能用于分配与第一次申请时大小相同的内存,例如,一个4K的Page,如果第一次分配了1K的内存,那么后面这个Page就只能继续分配1K的内存,如果有一个申请2K内存的请求,就需要在一个新的Page中进行分配。
Page中存储区域的使用状态通过一个long数组来维护,数组中每个long的每一位表示一个块存储区域的占用情况:0表示未占用,1表示占用。例如:对于一个4K的Page来说如果这个Page用来分配1K的存储与区,那么long数组中就只有一个long类型的元素且这个数值的低4危用来指示4个存储区域的占用情况。
回到如上所示的关于PoolSubpage的字段和构造函数的代码中:
在PoolSubpage的实现中,使用的是字段private final long[] bitmap;
来记录Page的使用状态,其中bitmap数组的最大长度为:pageSize / 16 / 64,这里的16指的是块的最小值,64是long类型的所占的bit数。
而存储块的大小使用的是字段elemSize来记录,当第一次在这个page上申请小于pageSize的内存时将调用如下的init函数来记录相关的块信息,例如:块的大小、块的个数、初始化bitmap等。
void init(int elemSize) {
doNotDestroy = true;
this.elemSize = elemSize;
if (elemSize != 0) {
maxNumElems = numAvail = pageSize / elemSize;
nextAvail = 0;
bitmapLength = maxNumElems >>> 6;//等价于bitmapLength = maxNumElems / 64;64为long类型所占的bit数
if ((maxNumElems & 63) != 0) { //如果块的个数不是64的整倍数,则加 1
bitmapLength ++;
}
for (int i = 0; i < bitmapLength; i ++) {
bitmap[i] = 0;
}
}
addToPool();
}
继续看addToPool()方法
该方法的功能为:将当前的Page加入到PoolArena所持有的PoolSubpage<T>[] tinySubpagePools;
或PoolSubpage<T>[] smallSubpagePools
数组中,为什么要加入到这里面来呢??
这是因为PoolSubpage[] tinySubpagePools,数组默认长度为32(512 >>4),这里面存储的Page是专门用来分配小内存tiny(小于512),smallSubpagePools数组中存储的Page则是用来分配small(大于等于512小于pageSize)内存的。
private void addToPool() {
PoolSubpage<T> head = chunk.arena.findSubpagePoolHead(elemSize);
assert prev == null && next == null;
prev = head;
next = head.next;
next.prev = this;
head.next = this;
}
2、subpage.allocate()
/**
* Returns the bitmap index of the subpage allocation.
*/
long allocate() {
if (elemSize == 0) {
return toHandle(0);
}
//判断此page是否还有“块”可用,以及是否被销毁了,如果没有可用空间或者是被销毁了则返回-1.
if (numAvail == 0 || !doNotDestroy) {
return -1;
}
final int bitmapIdx = getNextAvail();
int q = bitmapIdx >>> 6;
int r = bitmapIdx & 63;
assert (bitmap[q] >>> r & 1) == 0;//此bit位此时应该为0.
bitmap[q] |= 1L << r;//将bitmap[q]这个long型的数的第rbit置为1,标识此“块”已经被分配。
if (-- numAvail == 0) {
removeFromPool();
}
return toHandle(bitmapIdx);
}
该函数主要逻辑为:
1、首先通过getNextAvail()方法来得到此Page中下一个可用“块”的位置bitmapIdx。至于如何来得到,稍后将分析。
2、将bitmapIdx“可用块”在bitmap中标识为“已占用”的状态。具体如何来做的呢?
2.1)、根据q = bitmapIdx >>> 6
和r = bitmapIdx & 63
两行代码得到第bitmapIdx这个可用“内存块”在bitmap标识数组中是第q个long元素且是第q个元素的第r为来进行标识的。例如:假设bitmapIdx=66,则q=1,r=2,即是用bitmap[1]这个long类型数的第2个bit位来表示此“内存块”的。
2.2)、利用assert (bitmap[q] >>> r & 1) == 0
判断分配前bitmap[q]第r(bit)位一定是0,0:未占用。由于马上就将此“内存块”分配出去,因此利用bitmap[q] |= 1L << r
将bitmap[q]第r(bit)位置为1,1:占用。
3、将page的可用“块数”numAvail减一,减一之后如果结果为0,则表示此Page的内存无可分配的了,因此,将其从Arena所持有的链表中移除。
下面来看下getNextAvail()方法是如何得到此Page中下一个可用“块”的位置bitmapIdx的?
先猜测一下:对标识数组bitmap的每一个long元素的每一位进行判断,看是否为0,0表示为占用。
private int getNextAvail() {
int nextAvail = this.nextAvail;
if (nextAvail >= 0) {//page被第一次申请可用“块”的时候nextAvail=0,会直接返回。表示直接用第0位内存块
this.nextAvail = -1;
return nextAvail;
}
return findNextAvail();
}
如果是第2、3…来申请时,则会调用如下的findNextAvail()来实现。下面的代码比较简单,和猜测的一样,确实是通过按顺序遍历判断标识数组bitmap的每一个long元素的每一个bit是否为零来得到。不过下面的代码有一点值得我们学习:将位操作应用的淋漓尽致。
private int findNextAvail() {
final long[] bitmap = this.bitmap;
final int bitmapLength = this.bitmapLength;
//对标识数组bitmap中的每一个long元素进行判断
for (int i = 0; i < bitmapLength; i ++) {
long bits = bitmap[i];
if (~bits != 0) {//还存在至少1个bit位不为1,1表示占用
return findNextAvail0(i, bits);
}
}
return -1;
}
private int findNextAvail0(int i, long bits) {
final int maxNumElems = this.maxNumElems;
final int baseVal = i << 6;
//对long类型的数的每一bit位进行判断。
for (int j = 0; j < 64; j ++) {
if ((bits & 1) == 0) { //第j(bit)为0,即为占用
int val = baseVal | j;
if (val < maxNumElems) {
return val;
} else {
break;
}
}
bits >>>= 1;
}
return -1;
}
小结
分析完PoolSubpage这个类,我们需要了解3点:
1、PoolSubpage这个类的块大小是由第一次申请的内存大小来决定的。
2、PoolSubpage这个类是通过long类型的数组bitmap来对PoolSubPage中的每个块的使用情况进行标识的。
3、如果想在某个PoolSubpage分配一个小于pageSize的内存,则首先是通过按顺序遍历标识数组bitmap中每个long元素中的每一bit位中为0的位置。