Netty原始碼解析 -- PoolChunk實現原理(jemalloc 3的演算法)

binecy發表於2020-12-26

前面文章已經分享了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》。

如果您覺得本文不錯,歡迎關注我的微信公眾號,系列文章持續更新中。您的關注是我堅持的動力!

相關文章