netty4之後,netty中加入了內存池管理,看了netty後突然對這塊很感興趣,怎麼辦?最簡單的方式無非看源碼唄。我們順着思路強行分析一波。分析下簡單需求,爲了能夠簡單的操作內存,必須保證每次分配到的內存時連續的。我們來看下netty的poolChunk。
PoolChunk
該類主要負責內存塊的分配與回收,我們可以從源碼中給的註釋差不多看清PoolChunk的數據結構與算法。先介紹兩個術語:
page-是可以分配的內存塊的最小單位
chunk-是一堆page的集合
下面是chunk的構造方法
PoolChunk(PoolArena<T> arena, T memory, int pageSize, int maxOrder, int pageShifts, int chunkSize, int offset) {
unpooled = false;
this.arena = arena;
// memory是一個容量爲chunkSize的byte[](heap方式)或ByteBuffer(direct方式)
this.memory = memory;
// 每個page的大小,默認爲8192,8k
this.pageSize = pageSize;
// 13
this.pageShifts = pageShifts;
// 11層
this.maxOrder = maxOrder;
// 8k * 2048
this.chunkSize = chunkSize;
this.offset = offset;
unusable = (byte) (maxOrder + 1);
log2ChunkSize = log2(chunkSize);
// -8192
subpageOverflowMask = ~(pageSize - 1);
freeBytes = chunkSize;
assert maxOrder < 30 : "maxOrder should be < 30, but is: " + maxOrder;
maxSubpageAllocs = 1 << maxOrder;
// Generate the memory map.
memoryMap = new byte[maxSubpageAllocs << 1];
depthMap = new byte[memoryMap.length];
int memoryMapIndex = 1;
for (int d = 0; d <= maxOrder; ++ d) { // move down the tree one level at a time
int depth = 1 << d;
for (int p = 0; p < depth; ++ p) {
// in each level traverse left to right and set value to the depth of subtree
memoryMap[memoryMapIndex] = (byte) d;
depthMap[memoryMapIndex] = (byte) d;
memoryMapIndex ++;
}
}
subpages = newSubpageArray(maxSubpageAllocs);
}
可能光看構造我們會摸不清頭腦,我去網上挖了張圖,方便大家理解。這圖跟上圖的區別無非把節點的id換成了層數。其實完全二叉樹天生就可以用數組來表示,於是id跟層數對應起來可以用數組memoryMap表示。數組下標爲id,值爲層數。depthMap也是這樣存的,但它初始化後數據不變了,僅僅查表作用。poolChunk維護memoryMap主要用來標記使用情況。
1) memoryMap[id] = depth_of_id => it is free / unallocated
* 2) memoryMap[id] > depth_of_id => at least one of its child nodes is allocated, so we cannot allocate it, but
* some of its children can still be allocated based on their availability
* 3) memoryMap[id] = maxOrder + 1 => the node is fully allocated & thus none of its children can be allocated, it
* is thus marked as unusable
順便舉個例子吧,我們看下id爲512的節點,它層數爲9,此時memoryMap[512]=9,說明512節點下面的page一個都沒分配,如果此時我們分配了一塊page1(隨便抽的),那麼按照算法,memoryMap[2049]=12,memoryMap[1024]=11,memoryMap[512]=10。一直更新上去。後面會具體從代碼層面分析。
道理講了這麼多,看代碼吧。構造函數之前都是簡單賦值,大小我也註釋都標上了,我們看下下面這塊代碼,其實邏輯也很簡單,無非初始化memoryMap跟depthMap,此時可以注意到memoryMap的第一個節點即memoryMap[0]=0,這個元素是空出來的,下標爲1的元素纔是代表第一個節點,這樣也有好處,下標爲i的父元素的左孩子就是2*i(i<<1),右孩子呢? 2*i+1 ((i<<1)^1)。位運算效率總是討人喜歡的。
memoryMap = new byte[maxSubpageAllocs << 1];
depthMap = new byte[memoryMap.length];
int memoryMapIndex = 1;
for (int d = 0; d <= maxOrder; ++ d) { // move down the tree one level at a time
int depth = 1 << d;
for (int p = 0; p < depth; ++ p) {
// in each level traverse left to right and set value to the depth of subtree
memoryMap[memoryMapIndex] = (byte) d;
depthMap[memoryMapIndex] = (byte) d;
memoryMapIndex ++;
}
}
subpages = newSubpageArray(maxSubpageAllocs);
最後無非賦初值subpages。
poolChunk的數據結構差不多說清楚了,具體來看下如何向PoolChunk申請空間的
allocate()函數
long allocate(int normCapacity) {
if ((normCapacity & subpageOverflowMask) != 0) {
return allocateRun(normCapacity);
} else {
return allocateSubpage(normCapacity);
}
}
一開始的位運算就用的很討人喜歡,(normCapacity & subpageOverflowMask) != 0,normCapacity無非是申請空間的字節數大小,我們可以從構造函數中看到 subpageOverflowMask = ~(pageSize - 1); 那麼subpageOverflowMask大小爲-8192(光看數值看不出所以然,它在內存中是最高位跟低13位全爲0,剩下全爲1)那麼很容易想到,這句話意思無非等價於normCapacity >= pageSize;
然後分兩種情況,當申請大小大於pageSize,那麼一塊page肯定不夠,於是需要多塊,返回的是可分配normCapacity大小的節點的id;第二中情況,一個page已經夠分了,但是實際應用中會存在很多小塊內存的分配,如果小塊內存也佔用一個page明顯浪費,於是page也可以繼續拆分了,這是後話。我們先來看下allocateRun()
private long allocateRun(int normCapacity) {
int d = maxOrder - (log2(normCapacity) - pageShifts);
int id = allocateNode(d);
if (id < 0) {
return id;
}
freeBytes -= runLength(id);
return id;
}
d表示申請normCapacity大小對應的節點的深度,通過很簡單的運算即(normCapacity/8k)再取對數,拿maxOrder(11)減一下。
private int allocateNode(int d) {
int id = 1;
int initial = - (1 << d); // has last d bits = 0 and rest all = 1
byte val = value(id);
if (val > d) { // unusable
return -1;
}
while (val < d || (id & initial) == 0) { // id & initial == 1 << d for all ids at depth d, for < d it is 0
id <<= 1;
val = value(id);
if (val > d) {
id ^= 1;
val = value(id);
}
}
byte value = value(id);
assert value == d && (id & initial) == 1 << d : String.format("val = %d, id & initial = %d, d = %d",
value, id & initial, d);
setValue(id, unusable); // mark as unusable
updateParentsAlloc(id);
return id;
}
id賦初值爲1,從根節點開始找,如果(val > d)第一層的節點的值大於d,表示d層及以上都不可分配,說明當前Chunk取不出那麼大的空間,此次分配失敗。一直在樹上找直到找到與d恰好匹配的節點,即高度不小於d並且最小可分配,id<<=1往下一層走;當前節點對應大小不夠分配,則id^=1切換到父節點的另一子節點中(若父節點夠分配,則此節點肯定能分配);找到對應節點id後,把當前節點的值設爲不可用,並且遞歸循環調整父節點的MemoryMap的值
private void updateParentsAlloc(int id) {
while (id > 1) {
int parentId = id >>> 1;
byte val1 = value(id);
byte val2 = value(id ^ 1);
byte val = val1 < val2 ? val1 : val2;
setValue(parentId, val);
id = parentId;
}
}
代碼邏輯很簡單,父節點值設爲子節點中的最小值。直到根節點,調整結束。
我們再看當申請大小小於pageSize的第二種情況:
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();
}
}
針對這種情況,可以將8K的page拆成更小的塊,這已經超出chunk的管理範圍了,這個時候就出現了PoolSubpage, 通過normCapacity大小找到對應的PoolSubpage頭結點,然後加鎖(分配過程會改變鏈表結構),d=maxOrder,因此此時只需一塊page足夠,所以d直接設置爲maxOrder即葉子節點就ok,通過allcNode()分配到節點的id,如果id小於0表示分配失敗(葉子節點分配完了)返回。之後找到最高的合適節點後,開始新建(並加入到poolChunk的subpages數組中)或初始化subpage,然後調用subpage的allocate()申請空間,返回了個handle;下一篇博文會將subpage的具體邏輯。
我們再來看下釋放空間,free()函數。
void free(long handle) {
int memoryMapIdx = memoryMapIdx(handle);
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;
}
}
}
freeBytes += runLength(memoryMapIdx);
setValue(memoryMapIdx, depth(memoryMapIdx));
updateParentsFree(memoryMapIdx);
}
其中傳入的handle無非是allocate得到的handle,通過handle得到page的id,如果通過handle計算得到的bitmapIdx爲0,則表示是之前申請空間的第一種條件,大小大於pageSize,於是直接釋放整個節點對應的page空間,則將其memoryMap[id]=其對應的層數(depth[id]),於是調整父親節點,此時是申請時的逆過程。
private void updateParentsFree(int id) {
int logChild = depth(id) + 1;
while (id > 1) {
int parentId = id >>> 1;
byte val1 = value(id);
byte val2 = value(id ^ 1);
logChild -= 1; // in first iteration equals log, subsequently reduce 1 from logChild as we traverse up
if (val1 == logChild && val2 == logChild) {
setValue(parentId, (byte) (logChild - 1));
} else {
byte val = val1 < val2 ? val1 : val2;
setValue(parentId, val);
}
id = parentId;
}
}
如果左右孩子value值一樣,則父節點value設爲孩子的value-1,否則設爲孩子節點的最小值。
第二種情況,則需釋放subpage再繼續上面介紹的釋放page。
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;
}
}
}
這兒有涉及到subpage部分不是講得很清,沒關係,以poolChunk爲netty內存池的切入點。接下來會,向下細挖subpage,向上探索poolArea等等。