前面文章已經分享了Netty如何實現jemalloc 4算法管理內存。
本文主要分享Netty 4.1.52以前版本中,PoolChunk如何使用jemalloc 3算法管理內存。
感興趣的同窗能夠對比兩種算法。
源碼分析基於Netty 4.1.29算法
首先說明PoolChunk內存組織方式。
PoolChunk的內存大小默認是16M,它將內存組織成爲一顆完美二叉樹。
二叉樹的每一層每一個節點所表明的內存大小都是均等的,而且每一層節點所表明的內存大小總和加起來都是16M。
每一層節點可分配內存是父節點的1/2。整顆二叉樹的總層數爲12,層數從0開始。數組
示意圖以下
微信
先看一下PoolChunk的構造函數jvm
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)
。this
PoolChunk#allocate3d
long allocate(int normCapacity) { if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize return allocateRun(normCapacity); } else { return allocateSubpage(normCapacity); } }
若申請內存大於pageSize,調用allocateRun方法分配Chunk級別的內存。
不然調用allocateSubpage方法分配PoolSubpage,再在PoolSubpage上分配所需內存。rest
PoolChunk#allocateRuncode
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》。
若是您以爲本文不錯,歡迎關注個人微信公衆號,系列文章持續更新中。您的關注是我堅持的動力!