上一節(jié)中分析了如何在poolChunk中分配一塊大于pageSize的內(nèi)存,但在實際應(yīng)用中,存在很多分配小內(nèi)存的情況,如果也占用一個page,明顯很浪費。針對這種情況,Netty提供了PoolSubpage把poolChunk的一個page節(jié)點8k內(nèi)存劃分成更小的內(nèi)存段,通過對每個內(nèi)存段的標(biāo)記與清理標(biāo)記進行內(nèi)存的分配與釋放。
final class PoolSubpage<T> {
// 當(dāng)前page在chunk中的id
private final int memoryMapIdx;
// 當(dāng)前page在chunk.memory的偏移量
private final int runOffset;
// page大小
private final int pageSize;
//通過對每一個二進制位的標(biāo)記來修改一段內(nèi)存的占用狀態(tài)
private final long[] bitmap;
PoolSubpage<T> prev;
PoolSubpage<T> next;
boolean doNotDestroy;
// 該page切分后每一段的大小
int elemSize;
// 該page包含的段數(shù)量
private int maxNumElems;
private int bitmapLength;
// 下一個可用的位置
private int nextAvail;
// 可用的段數(shù)量
private int numAvail;
...
}
假設(shè)目前需要申請大小為4096的內(nèi)存:
long allocate(int normCapacity) {
if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
return allocateRun(normCapacity);
} else {
return allocateSubpage(normCapacity);
}
}
因為4096<pageSize(8192),所以采用allocateSubpage進行內(nèi)存分配,具體實現(xiàn)如下:
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();
}
}
1、Arena負責(zé)管理PoolChunk和PoolSubpage;
2、allocateNode負責(zé)在二叉樹中找到匹配的節(jié)點,和poolChunk不同的是,只匹配葉子節(jié)點;
3、poolChunk中維護了一個大小為2048的poolSubpage數(shù)組,分別對應(yīng)二叉樹中2048個葉子節(jié)點,假設(shè)本次分配到節(jié)點2048,則取出poolSubpage數(shù)組第一個元素subpage;
4、如果subpage為空,則進行初始化,并加入到poolSubpage數(shù)組;
subpage初始化實現(xiàn)如下:
PoolSubpage(PoolSubpage<T> head,
PoolChunk<T> chunk,
int memoryMapIdx, int runOffset,
int pageSize, elemSize) {
this.chunk = chunk;
this.memoryMapIdx = memoryMapIdx;
this.runOffset = runOffset;
this.pageSize = pageSize;
bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64
init(head, elemSize);
}
1、默認初始化bitmap長度為8,這里解釋一下為什么只需要8個元素:其中分配內(nèi)存大小都是處理過的,最小為16,說明一個page可以分成8192/16 = 512個內(nèi)存段,一個long有64位,可以描述64個內(nèi)存段,這樣只需要512/64 = 8個long就可以描述全部內(nèi)存段了。
2、init根據(jù)當(dāng)前需要分配的內(nèi)存大小,確定需要多少個bitmap元素,實現(xiàn)如下:
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);
}
下面通過分布申請4096和32大小的內(nèi)存,說明如何確定bitmapLength的值:
- 比如,當(dāng)前申請大小4096的內(nèi)存,maxNumElems 和 numAvail 為2,說明一個page被拆分成2個內(nèi)存段,2 >>> 6 = 0,且2 & 63 != 0,所以bitmapLength為1,說明只需要一個long就可以描述2個內(nèi)存段狀態(tài)。
- 如果當(dāng)前申請大小32的內(nèi)存,maxNumElems 和 numAvail 為 256,說明一個page被拆分成256個內(nèi)存段, 256>>> 6 = 4,說明需要4個long描述256個內(nèi)存段狀態(tài)。
下面看看subpage是如何進行內(nèi)存分配的:
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);
}
1、方法getNextAvail負責(zé)找到當(dāng)前page中可分配內(nèi)存段的bitmapIdx;
2、q = bitmapIdx >>> 6,確定bitmap數(shù)組下標(biāo)為q的long數(shù),用來描述 bitmapIdx 內(nèi)存段的狀態(tài);
3、bitmapIdx & 63將超出64的那一部分二進制數(shù)抹掉,得到一個小于64的數(shù)r;
4、bitmap[q] |= 1L << r將對應(yīng)位置q設(shè)置為1;
如果以上描述不直觀的話,下面換一種方式解釋,假設(shè)需要分配大小為128的內(nèi)存,這時page會拆分成64個內(nèi)存段,需要1個long類型的數(shù)字描述這64個內(nèi)存段的狀態(tài),所以bitmap數(shù)組只會用到第一個元素。
getNextAvail如何實現(xiàn)找到下一個可分配的內(nèi)存段?
private int getNextAvail() {
int nextAvail = this.nextAvail;
if (nextAvail >= 0) {
this.nextAvail = -1;
return nextAvail;
}
return findNextAvail();
}
1、如果nextAvail大于等于0,說明nextAvail指向了下一個可分配的內(nèi)存段,直接返回nextAvail值;
2、每次分配完成,nextAvail被置為-1,這時只能通過方法findNextAvail重新計算出下一個可分配的內(nèi)存段位置。
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;
}
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;
}
1、~bits != 0說明這個long所描述的64個內(nèi)存段還有未分配的;
2、(bits & 1) == 0 用來判斷該位置是否未分配,否則bits又移一位,從左到右遍歷值為0的位置;
至此,subpage內(nèi)存段已經(jīng)分配完成。
END。
我是占小狼。
在魔都艱苦奮斗,白天是上班族,晚上是知識服務(wù)工作者。
如果讀完覺得有收獲的話,記得關(guān)注和點贊哦。
非要打賞的話,我也是不會拒絕的。