前言
Netty所謂的池化就是先申請了一塊大內存,後面需要分配的時候就來我這裏分就完了。以堆外直接內存分配爲例,Netty以Chunk爲單位16M申請了一塊連續內存,這麼一大塊內存是以平衡二叉樹的形式組織起來的。分配的時候就從這顆樹上找合適的節點。池化內存的分配是Netty的最爲核心部分,這塊的代碼很多位運算,不太容易看懂,讀的時候需要邊調試邊分析。
平衡二叉樹
Normal類型的組織,Netty使用平衡二叉樹將申請到的Chunk塊組織起來,如下圖所示,並使用數組將整個樹映射進去,見下文構造函數中memoryMap。
構造函數
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;
memoryMap = new byte[maxSubpageAllocs << 1];
depthMap = new byte[memoryMap.length];
int memoryMapIndex = 1;
for (int d = 0; d <= maxOrder; ++ d) {
int depth = 1 << d;
for (int p = 0; p < depth; ++ p) {
memoryMap[memoryMapIndex] = (byte) d;
depthMap[memoryMapIndex] = (byte) d;
memoryMapIndex ++;
}
}
subpages = newSubpageArray(maxSubpageAllocs);
cachedNioBuffers = new ArrayDeque<ByteBuffer>(8);
}
關鍵參數
參數 | 說明 |
---|---|
memory | 申請的一塊內存大小爲16M,以字節數組表示 |
pageSize | 8KB |
pageShifts | 13 |
maxOrder | 11 |
chunkSize | 16M |
log2ChunkSize | 14 |
subpageOverflowMask | -8192 |
maxSubpageAllocs | 2048 |
maxSubpageAllocs << 1 | memoryMap數組的長度,4096 |
memoryMap | 將二叉樹平鋪在該數組,格式見下文。 |
depthMap | 同memoryMap將二叉樹平鋪在該數組 |
上面這些有些是直接傳入的,有的一眼看不出結果,代入公式算算。
log2ChunkSize 這個是chunkSize的對數,chunkSize大小是16M
@Test
public void testlog2ChunkSize(){
System.out.println("16M的對數:" + log2(16384));
System.out.println("2的14次方:" + Math.pow(2, 14));
}
private static int log2(int val) {
int INTEGER_SIZE_MINUS_ONE = Integer.SIZE - 1;
// compute the (0-based, with lsb = 0) position of highest set bit i.e, log2
return INTEGER_SIZE_MINUS_ONE - Integer.numberOfLeadingZeros(val);
}
輸出:
16M的對數:14
2的14次方:16384.0
subpageOverflowMask 主要用於位運算的“&”操作判斷。
@Test
public void testSubpageOverflowMask(){
int pageSize = 8192;
int subpageOverflowMask = ~(pageSize - 1);
System.out.println("subpageOverflowMask:" + subpageOverflowMask);
}
輸出:
subpageOverflowMask:-8192
maxSubpageAllocs 用於限制數組的長度
@Test
public void testMaxSubpageAllocs(){
int maxOrder = 11;
int maxSubpageAllocs = 1 << maxOrder;
System.out.println("maxSubpageAllocs:" + maxSubpageAllocs);
int length = maxSubpageAllocs << 1;
System.out.println("memoryMap數組的長度:" + length);
}
輸出:
maxSubpageAllocs:2048
memoryMap數組的長度:4096
memoryMap&depthMap
這兩個數組主要把平衡二叉樹裝到了數組裏,外層循環控制層高,內層循環控制每層的節點數。直接看還是看不出到底啥格式,不要緊代入輸出看看。
@Test
public void testMemoryMap(){
int maxOrder = 11;
int memoryMapIndex = 1;
int maxSubpageAllocs = 2048;
byte[] memoryMap = new byte[maxSubpageAllocs << 1];
byte[] depthMap = new byte[memoryMap.length];
for (int d = 0; d <= maxOrder; ++ d) {
int depth = 1 << d;
for (int p = 0; p < depth; ++ p) {
memoryMap[memoryMapIndex] = (byte) d;
depthMap[memoryMapIndex] = (byte) d;
memoryMapIndex ++;
}
}
System.out.println("memoryMap的長度:" + memoryMap.length);
System.out.println("memoryMap數組內容:" + Arrays.toString(memoryMap));
}
輸出:
memoryMap的長度:4096
memoryMap數組內容:[0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, ..... ]
小結
由輸出可以看出memoryMap的長度爲4096,內容格式如上,值表示所在的層,例如:數字1表示該節點在第樹的第1層;有兩個1表示該層有兩個葉子節點。也就是從2的0次方,一直到2的11次方,平鋪到了數組中。
平衡二叉樹查找更新過程
三次分配示例
Normal類型的內存分配,主要是如何在二叉樹中找到匹配的節點的過程,以及該節點的被分配後整個樹的狀態更新變化。
源碼部分在PoolChunk#allocateRun(int normCapacity) 部分,爲了方便debug和調試,把值代入後單獨拎出來跑一跑。
下面代碼可以直接運行,以執行三次分配,每次分配8KB的過程來看其對平衡二叉樹的查找過程。
public class PoolChunkTest {
byte[] memoryMap = null;
private final byte unusable = (byte) (11 + 1);
public static void main(String [] args){
PoolChunkTest poolChunkTest = new PoolChunkTest();
poolChunkTest.initMemoryMap();
int normCapacity = 8 * 1024;
long id01 = poolChunkTest.allocateRun(normCapacity);
System.out.println("第一次分配:" + id01);
long id02 = poolChunkTest.allocateRun(normCapacity);
System.out.println("第二次分配:" + id02);
long id03 = poolChunkTest.allocateRun(normCapacity);
System.out.println("第三次分配:" + id03);
}
private long allocateRun(int normCapacity) {
int d = 11 - (log2(normCapacity) - 13);
int id = allocateNode(d);
return id;
}
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);
System.out.println("val:" + val);
}
}
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;
}
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;
}
}
private void setValue(int id, byte val) {
memoryMap[id] = val;
}
private static int log2(int val) {
int INTEGER_SIZE_MINUS_ONE = Integer.SIZE - 1;
// compute the (0-based, with lsb = 0) position of highest set bit i.e, log2
return INTEGER_SIZE_MINUS_ONE - Integer.numberOfLeadingZeros(val);
}
private byte value(int id) {
return memoryMap[id];
}
public void initMemoryMap(){
int maxOrder = 11;
int memoryMapIndex = 1;
int maxSubpageAllocs = 2048;
memoryMap = new byte[maxSubpageAllocs << 1];
byte[] depthMap = new byte[memoryMap.length];
for (int d = 0; d <= maxOrder; ++ d) {
int depth = 1 << d;
for (int p = 0; p < depth; ++ p) {
memoryMap[memoryMapIndex] = (byte) d;
depthMap[memoryMapIndex] = (byte) d;
memoryMapIndex ++;
}
}
}
}
輸出:
第一次分配:2048
第二次分配:2049
第三次分配:2050
例子中分配的8KB,根據公式 int d = 11 - (log2(normCapacity) - 13)算出其在11層,所以下文中三次分配時入參d=11。
第一分配8KB
第一次分配找到了數組memoryMap的下標爲2048,此時對應的值爲memoryMap[2048]=11。
當分配後將該節點標記爲不可用,也就是更新爲12(總共才11層,所以12爲不可用),此時memoryMap[2048]=12。
遞歸整棵樹,從下往上更新直到根節點,將父節點更新爲其子節點的最小值。例如:memoryMap[1024]的值原來爲10,被更新成了11.
第二次分配8KB
先找到了id=2048,這個節點發現其值爲12,也就是不可用了。此時memoryMap[2049]=11。
通過id ^= 1找到其兄弟節點id=2049,其對應的值爲11可用。返回該節點。
![image-20210314185941227](/Users/yongliang/Library/Application Support/typora-user-images/image-20210314185941227.png)
將節點id=2049設置爲不可用,即:memoryMap[2049]=12。
遞歸更新整棵樹,由於其父節點的兩個子節點都被分配出去了,所以1024被標記爲不可用。memoryMap[1024]=12。
第三次分配8KB
第三次分配8KB時,當循環到了節點1024,發現其不可用,也就是其子節點也不可用了。
通過id <<= 1找到1024的兄弟節點1025,接着向下查找。
找到節點1025的子節點2050發現其可用,即:memoryMap[2050]=11。找到後最後過程同上,標記其不可用表示已分配了,並更新整個樹把其父節點更新爲子節點的最小值。
平衡二叉樹查找更新圖示
第一次分配8KB前
整個樹都沒有被佔用,8KB會被選在第11層分配。
第一次分配8KB後
紅色標記爲變化部分,節點紅色表示被佔用。第11層的第一個節點memoryMap[2048]被標記爲不可用。第10層的第一個節點memoryMap[1024]值被從10更新爲11,整個樹會繼續向上遞歸變化,將整個樹父節點更新爲子節點最小的值。
第二次分配8KB後
第二次分配8KB後,第11層的第二個節點memoryMap[2049]被標記爲不可用,其父節點memoryMap[1024]由於其子節點都被分配完畢,也被標記爲不可用。整個樹會繼續向上遞歸變化,將整個樹父節點更新爲子節點最小的值。
第三次分配8KB後
第三次分配8KB後,將第11層的第三個節點memoryMap[2050]標記爲不可用,同時更新其父節點memoryMap[1025]爲子節點最小值11。整個樹會繼續向上遞歸變化,將整個樹父節點更新爲子節點最小的值。
本文分享自微信公衆號 - 瓜農老梁(gh_01130ae30a83)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。