前面文章已經分享了Netty如何實現jemalloc 4演算法管理記憶體。
本文主要分享Netty 4.1.52之前版本中,PoolChunk如何使用jemalloc 3演算法管理記憶體。
感興趣的同學可以對比兩種演算法。
原始碼分析基於Netty 4.1.29
首先說明PoolChunk記憶體組織方式。
PoolChunk的記憶體大小預設是16M,它將記憶體組織成為一顆完美二叉樹。
二叉樹的每一層每個節點所代表的記憶體大小都是均等的,並且每一層節點所代表的記憶體大小總和加起來都是16M。
每一層節點可分配記憶體是父節點的1/2。整顆二叉樹的總層數為12,層數從0開始。
示意圖如下
先看一下PoolChunk的建構函式
PoolChunk(PoolArena<T> arena, T memory, int pageSize, int maxOrder, int pageShifts, int chunkSize, int offset) {
unpooled = false;
this.arena = arena;
this.memory = memory;
this.pageSize = pageSize;
this.pageShifts = pageShifts;
this.maxOrder = maxOrder;
this.chunkSize = chunkSize;
this.offset = offset;
unusable = (byte) (maxOrder + 1);
log2ChunkSize = log2(chunkSize);
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);
}
unpooled: 是否使用記憶體池
arena:該PoolChunk所屬的PoolArena
memory:底層的記憶體塊,對於堆記憶體,它是一個byte陣列,對於直接記憶體,它是(jvm)ByteBuffer,但無論是哪種形式,其記憶體大小預設都是16M。
pageSize:葉子節點大小,預設為8192,即8K。
maxOrder:表示二叉樹最大的層數,從0開始。預設為11。
chunkSize:整個PoolChunk的記憶體大小,預設為16777216,即16M。
offset:底層記憶體對齊偏移量,預設為0。
unusable:表示節點已被分配,不用了,預設為12。
freeBytes:空閒記憶體位元組數。
每個PoolChunk都要按記憶體使用率關聯到一個PoolChunkList上,記憶體使用率正是通過freeBytes計算。
maxSubpageAllocs:葉子節點數量,預設為2048,即2^11。
log2ChunkSize:用於計算偏移量,預設為24。
subpageOverflowMask:用於判斷申請記憶體是否為PoolSubpage,預設為-8192。
pageShifts:用於計算分配記憶體所在二叉樹層數,預設為13。
memoryMap:初始化記憶體管理二叉樹,將每一層節點值設定為層數d。
使用陣列維護二叉樹,第d層的開始下標為 1<<d
。(陣列第0個元素不使用)。
depthMap:儲存二叉樹的層數,用於通過位置下標找到其在整棵樹中對應的層數。
注意:depthMap的值代表二叉樹的層數,初始化後不再變化。
memoryMap的值代表當前節點最大可申請記憶體塊,在分配記憶體過程中不斷變化。
節點最大可申請記憶體塊可以通過層數d計算,為2 ^ (pageShifts + maxOrder - d)
。
PoolChunk#allocate
long allocate(int normCapacity) {
if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
return allocateRun(normCapacity);
} else {
return allocateSubpage(normCapacity);
}
}
若申請記憶體大於pageSize,呼叫allocateRun方法分配Chunk級別的記憶體。
否則呼叫allocateSubpage方法分配PoolSubpage,再在PoolSubpage上分配所需記憶體。
PoolChunk#allocateRun
private long allocateRun(int normCapacity) {
// #1
int d = maxOrder - (log2(normCapacity) - pageShifts);
// #2
int id = allocateNode(d);
if (id < 0) {
return id;
}
// #2
freeBytes -= runLength(id);
return id;
}
#1
計算應該在哪層分配分配記憶體
maxOrder - (log2(normCapacity) - pageShifts)
,如16K, 即2^14,計算結果為10,即在10層分配。
#2
減少空閒記憶體位元組數。
PoolChunk#allocateNode,在d層分配一個節點
private int allocateNode(int d) {
int id = 1;
int initial = - (1 << d); // has last d bits = 0 and rest all = 1
// #1
byte val = value(id);
if (val > d) { // unusable
return -1;
}
// #2
while (val < d || (id & initial) == 0) { // id & initial == 1 << d for all ids at depth d, for < d it is 0
// #3
id <<= 1;
val = value(id);
// #4
if (val > d) {
// #5
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);
// #6
setValue(id, unusable); // mark as unusable
// #7
updateParentsAlloc(id);
return id;
}
#1
memoryMap[1] > d,第0層的可分配記憶體不足,表明該PoolChunk記憶體不能滿足分配,分配失敗。
#2
遍歷二叉樹,找到滿足記憶體分配的節點。
val < d
,即該節點記憶體滿足分配。
id & initial = 0
,即 id < 1<<d
, d層之前迴圈繼續執行。這裡並不會出現val > d的場景,但會出現val == d的場景,如
PoolChunk當前可分配記憶體為2M,即memoryMap[1] = 3,這時申請2M記憶體,在0-2層,都是val == d。可參考後面的例項。
#3
向下找到下一層下標,注意,子樹左節點的下標是父節點下標的2倍。
#4
val > d
,表示當前節點不能滿足分配
#5
id ^= 1
,查詢同一父節點下的兄弟節點,在兄弟節點上分配記憶體。
id ^= 1
,當id為偶數,即為id+=1
, 當id為奇數,即為id-=1
。
由於前面通過id <<= 1
找到下一層下標都是偶數,這裡等於id+=1。
#6
因為一開始判斷了PoolChunk記憶體是否足以分配,所以這裡一定可以找到一個可分配節點。
這裡標註找到的節點已分配。
#7
更新找到節點的父節點最大可分配記憶體塊大小
private void updateParentsAlloc(int id) {
// #1
while (id > 1) {
// #2
int parentId = id >>> 1;
byte val1 = value(id);
byte val2 = value(id ^ 1);
byte val = val1 < val2 ? val1 : val2;
setValue(parentId, val);
id = parentId;
}
}
#1
向父節點遍歷,直到根節點
#2
id >>> 1,找到父節點
取當前節點和兄弟節點中較小值,作為父節點的值,表示父節點最大可分配記憶體塊大小。
如memoryMap[1] = 0,表示最大可分配記憶體塊為16M。
分配8M後,memoryMap[1] = 1,表示當前最大可分配記憶體塊為8M。
下面看一則例項,大家可以結合例項理解上面的程式碼
記憶體釋放
PoolChunk#free
void free(long handle) {
// #1
int memoryMapIdx = memoryMapIdx(handle);
int bitmapIdx = bitmapIdx(handle);
// #2
if (bitmapIdx != 0) { // free a subpage
...
}
freeBytes += runLength(memoryMapIdx);
setValue(memoryMapIdx, depth(memoryMapIdx));
updateParentsFree(memoryMapIdx);
}
#1
獲取memoryMapIdx和bitmapIdx
#2
記憶體塊在PoolSubpage中分配,通過PoolSubpage釋放記憶體。
#3
處理到這裡,就是釋放Chunk級別的記憶體塊了。
增加空閒記憶體位元組數。
設定二叉樹中對應的節點為未分配
對應修改該節點的父節點。
另外,Netty 4.1.52對PoolArena記憶體級別劃分的演算法也做了調整。
Netty 4.1.52的具體演算法前面文章《Netty記憶體池與PoolArena》已經說過了,這裡簡單說一下Netty 4.1.52前的演算法。
PoolArena中將維護的記憶體塊按大小劃分為以下級別:
Tiny < 512
Small < 8192(8K)
Chunk < 16777216(16M)
Huge >= 16777216
PoolArena#tinySubpagePools,smallSubpagePools兩個陣列用於維護Tiny,Small級別的記憶體塊。
tinySubpagePools,32個元素,每個陣列之間差16個位元組,大小分別為0,16,32,48,64, ... ,496
smallSubpagePools,4個元素,每個陣列之間大小翻倍,大小分別為512,1025,2048,4096
這兩個陣列都是PoolSubpage陣列,PoolSubpage大小預設都是8192,Tiny,Small級別的記憶體都是在PoolSubpage上分配的。
Chunk記憶體塊則都是8192的倍數。
在Netty 4.1.52,已經刪除了Small級別記憶體塊,並引入了SizeClasses對齊記憶體塊或計算對應的索引。
SizeClasses預設將16M劃分為75個記憶體塊size,記憶體劃分更細,也可以減少記憶體對齊的空間浪費,更充分利用記憶體。感興趣的同學可以參考前面的文章《記憶體對齊類SizeClasses》。
如果您覺得本文不錯,歡迎關注我的微信公眾號,系列文章持續更新中。您的關注是我堅持的動力!