前言
前面聊了大於8KB的內存分配,那小於8KB的呢?上一篇的平衡二叉樹第十一層的葉子節點最小也是8KB,那比如要分配128B的緩存,直接分給8KB顯然是不合適的,Tiny是小於512Byte,Small介於512B~8KB,Tiny和Small統稱Subpage,本文就聊聊他們的內存分配情況,這塊應該是整個netty最爲複雜的部分了。
內容提要
下面是以分配128B爲例的整體流程架構圖,下面大體敘述下其流程。
-
先從平衡二叉樹的第11層選一個未分配的葉子節點大小爲8KB的一個Page
備註:本例中爲memoryMap[2048] -
對該Page進行切割,假如要分配128B,整體會切割爲64塊
備註:8192/128=64 -
通過long類型二進制64位來標記分割成各個塊的分配狀態
備註:0:未分配,1:已分配
-
一個bitmap數組長度爲8,每個元素都能對64塊內存進行標記
-
建立了二叉樹節點與切分塊之間的映射關係
備註:memoryMapIdx ^ maxSubpageAllocs -
分配後建立二叉樹葉子節點與標記位之間的關係,可以指向內存一塊區域
備註:0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx
源碼分析
示例代碼
@Test
public void testAllocateSubpage() {
ByteBufAllocator allocator = new PooledByteBufAllocator();
allocator.directBuffer(128);
}
備註:以分配128B的內存爲例,分析其分配過程。
源碼分析
private long allocateSubpage(int normCapacity) {
PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity); // 註解@1
int d = maxOrder;
synchronized (head) {
int id = allocateNode(d); // 註解@2
if (id < 0) {
return id;
}
final PoolSubpage<T>[] subpages = this.subpages; // 註解@3
final int pageSize = this.pageSize;
freeBytes -= pageSize;
int subpageIdx = subpageIdx(id); // 註解@4
PoolSubpage<T> subpage = subpages[subpageIdx];
if (subpage == null) { // 註解@5
subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
subpages[subpageIdx] = subpage;
} else {
subpage.init(head, normCapacity);
}
return subpage.allocate(); // 註解@6
}
}
註解@1 從tinySubpagePools中獲取PoolSubpage。獲取過程爲elemSize >>> 4(除以16)來獲取。
tinySubpagePools結構
tinySubpagePools被初始化成長度爲32的數組,元素之間差額爲16B。
註解@2 allocateNode 在上一篇文章分析過,d = maxOrder = 1。表示在平衡二叉樹的第11層找到可分配的節點,具體爲memoryMap數組中的下標。如果整個樹都沒有內存可分配了,返回的id=-1。
註解@3 先看下subpages的初始化,maxSubpageAllocs = 1 << maxOrder= 2048。也就是PoolSubpage
subpages = newSubpageArray(maxSubpageAllocs);
註解@4 將平衡二叉樹第11層的下標memoryMap[]的下標轉換爲subpages[]數組的下標。轉換關係爲memoryMapIdx ^ maxSubpageAllocs。
例如:平衡二叉樹第11層第1個節點數組下標爲2048,轉換爲subpages的下標爲0,平衡二叉樹第11層第2個節點數組下標爲2049,轉換爲subpages的下標爲1,平衡二叉樹第11層第2個節點數組下標爲2050,轉換爲subpages的下標爲2。
註解@5 初始化PoolSubpage
PoolSubpage(PoolSubpage<T> head, PoolChunk<T> chunk, int memoryMapIdx, int runOffset, int pageSize, int elemSize) {
this.chunk = chunk;
this.memoryMapIdx = memoryMapIdx;
this.runOffset = runOffset;
this.pageSize = pageSize;
bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64
init(head, elemSize);
}
參數說明
head: PoolSubpage數組中的一個元素,本例中爲第4個元素
chunk: 當前PoolChunk實例
memoryMapIdx: 平衡二叉樹第11層用於分配的節點,具體爲memoryMap數組下標
elemSize: 待分配的內存,本例中爲128KB
bitmap: long數組長度爲8「8192無符號右移10位=8」
初始化說明
void init(PoolSubpage<T> head, int elemSize) {
doNotDestroy = true;
// 待分配內存
this.elemSize = elemSize;
if (elemSize != 0) {
// maxNumElems表示可以被切割成幾份(8192除以待分配內存)例如:64=8192/128被切成了64份
maxNumElems = numAvail = pageSize / elemSize;
nextAvail = 0;
// 無符號右移6位,高位補零(相當於除以64)例如:64的二進制右移6位爲1,128的二進制右移6位爲2
bitmapLength = maxNumElems >>> 6;
if ((maxNumElems & 63) != 0) { // 相當於是否能被64整除
bitmapLength ++; // 不能被整除遞增bitmapLength
}
for (int i = 0; i < bitmapLength; i ++) {
bitmap[i] = 0; // 等於零表示未被分配
}
}
addToPool(head);
}
過程說明
@1 先計算一個Page被切成了幾份 maxNumElems( pageSize / elemSize)
@2 計算bitmap數組長度bitmapLength(maxNumElems無符號右移6位相當於除以64)
備註: 此處不太好理解爲什麼要maxNumElems要除以64來計算bitmap的長度呢?也就是bitmap數組中的每個元素可以標記64個被切的內存塊。bitmap是long數組,每個long類型是64位,他用每個二進制位來標記被切內存塊的分配情況。
加入鏈表
新構建的PoolSubpage與tinySubpagePools中的PoolSubpage建成鏈表關係。
private void addToPool(PoolSubpage<T> head) {
assert prev == null && next == null;
prev = head;
next = head.next;
next.prev = this;
head.next = this;
}
小結: 構造的PoolSubpage中持有了一個bitmap[]數組,數組長度與待分配的內存有關。待分配內存大小爲elemSize,數組長度=PageSize/elemSize,並將bitmap數組的元素標記爲未分配。
註解@6 分配內存
內存的分配以兩次分配128B內存爲例觀察期分配過程。
@Test
public void testAllocateSubpage() {
ByteBufAllocator allocator = new PooledByteBufAllocator();
allocator.directBuffer(128); // 第一次分配
allocator.directBuffer(128); // 第二次分配
}
第一次分配
第二次分配
第一次輪詢第一位已被佔用,需要向右移位。
第二次輪詢第二位未被佔用。
第二次分配過程
兩次內存分配圖示
第一次分配128B圖示
此時64位第一位被標記爲1,bitmap[0] = 1
第二次分配128B圖示
此時64位第二位也被標記爲1,bitmap[0] = 3
本文分享自微信公衆號 - 瓜農老梁(gh_01130ae30a83)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。