上一節(jié)中分析了如何在poolChunk中分配一塊大于pageSize的內(nèi)存,但在實(shí)際應(yīng)用中,存在很多分配小內(nèi)存的情況加袋,如果也占用一個(gè)page,明顯很浪費(fèi)抱既。針對(duì)這種情況职烧,Netty提供了PoolSubpage把poolChunk的一個(gè)page節(jié)點(diǎn)8k內(nèi)存劃分成更小的內(nèi)存段,通過對(duì)每個(gè)內(nèi)存段的標(biāo)記與清理標(biāo)記進(jìn)行內(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;
//通過對(duì)每一個(gè)二進(jìn)制位的標(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;
// 下一個(gè)可用的位置
private int nextAvail;
// 可用的段數(shù)量
private int numAvail;
...
}
假設(shè)目前需要申請(qǐng)大小為4096的內(nèi)存:
long allocate(int normCapacity) {
if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
return allocateRun(normCapacity);
} else {
return allocateSubpage(normCapacity);
}
}
因?yàn)?096<pageSize(8192)蚀之,所以采用allocateSubpage進(jìn)行內(nèi)存分配,具體實(shí)現(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負(fù)責(zé)管理PoolChunk和PoolSubpage足删;
2、allocateNode負(fù)責(zé)在二叉樹中找到匹配的節(jié)點(diǎn)锁右,和poolChunk不同的是失受,只匹配葉子節(jié)點(diǎn);
3咏瑟、poolChunk中維護(hù)了一個(gè)大小為2048的poolSubpage數(shù)組拂到,分別對(duì)應(yīng)二叉樹中2048個(gè)葉子節(jié)點(diǎn),假設(shè)本次分配到節(jié)點(diǎn)2048响蕴,則取出poolSubpage數(shù)組第一個(gè)元素subpage谆焊;
4、如果subpage為空浦夷,則進(jìn)行初始化辖试,并加入到poolSubpage數(shù)組;
subpage初始化實(shí)現(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劈狐、默認(rèn)初始化bitmap長度為8罐孝,這里解釋一下為什么只需要8個(gè)元素:其中分配內(nèi)存大小都是處理過的,最小為16肥缔,說明一個(gè)page可以分成8192/16 = 512個(gè)內(nèi)存段莲兢,一個(gè)long有64位,可以描述64個(gè)內(nèi)存段续膳,這樣只需要512/64 = 8個(gè)long就可以描述全部內(nèi)存段了改艇。
2、init根據(jù)當(dāng)前需要分配的內(nèi)存大小坟岔,確定需要多少個(gè)bitmap元素谒兄,實(shí)現(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);
}
下面通過分布申請(qǐng)4096和32大小的內(nèi)存,說明如何確定bitmapLength的值:
- 比如社付,當(dāng)前申請(qǐng)大小4096的內(nèi)存承疲,maxNumElems 和 numAvail 為2邻耕,說明一個(gè)page被拆分成2個(gè)內(nèi)存段,2 >>> 6 = 0燕鸽,且2 & 63 兄世!= 0,所以bitmapLength為1啊研,說明只需要一個(gè)long就可以描述2個(gè)內(nèi)存段狀態(tài)御滩。
- 如果當(dāng)前申請(qǐng)大小32的內(nèi)存,maxNumElems 和 numAvail 為 256悲伶,說明一個(gè)page被拆分成256個(gè)內(nèi)存段艾恼, 256>>> 6 = 4,說明需要4個(gè)long描述256個(gè)內(nèi)存段狀態(tài)麸锉。
下面看看subpage是如何進(jìn)行內(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負(fù)責(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的那一部分二進(jìn)制數(shù)抹掉,得到一個(gè)小于64的數(shù)r娩脾;
4赵誓、bitmap[q] |= 1L << r將對(duì)應(yīng)位置q設(shè)置為1;
如果以上描述不直觀的話柿赊,下面換一種方式解釋俩功,假設(shè)需要分配大小為128的內(nèi)存,這時(shí)page會(huì)拆分成64個(gè)內(nèi)存段碰声,需要1個(gè)long類型的數(shù)字描述這64個(gè)內(nèi)存段的狀態(tài)诡蜓,所以bitmap數(shù)組只會(huì)用到第一個(gè)元素。
getNextAvail如何實(shí)現(xiàn)找到下一個(gè)可分配的內(nèi)存段胰挑?
private int getNextAvail() {
int nextAvail = this.nextAvail;
if (nextAvail >= 0) {
this.nextAvail = -1;
return nextAvail;
}
return findNextAvail();
}
1蔓罚、如果nextAvail大于等于0,說明nextAvail指向了下一個(gè)可分配的內(nèi)存段瞻颂,直接返回nextAvail值豺谈;
2、每次分配完成贡这,nextAvail被置為-1茬末,這時(shí)只能通過方法findNextAvail重新計(jì)算出下一個(gè)可分配的內(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說明這個(gè)long所描述的64個(gè)內(nèi)存段還有未分配的团南;
2、(bits & 1) == 0 用來判斷該位置是否未分配炼彪,否則bits又移一位吐根,從左到右遍歷值為0的位置;
至此辐马,subpage內(nèi)存段已經(jīng)分配完成拷橘。
END。
我是占小狼喜爷。
在魔都艱苦奮斗冗疮,白天是上班族,晚上是知識(shí)服務(wù)工作者檩帐。
如果讀完覺得有收獲的話术幔,記得關(guān)注和點(diǎn)贊哦。
非要打賞的話湃密,我也是不會(huì)拒絕的诅挑。