前面我們介紹了PoolChunk以及針對page的更細粒度的PoolSubpage,其實在chunk的上層還有一個管理類:PoolChunkList,PoolChunkList負責管理多個chunk的生命週期,在此基礎上對內存分配進行進一步的優化,那它是如何去做的呢?我們來簡單的瞭解下,先看看它的幾個屬性:
//arena,整個內存池管理的大管家
private final PoolArena<T> arena;
//當前List本身就是一個連表,每個List相互連接
private final PoolChunkList<T> nextList;
private PoolChunkList<T> prevList;
// 當前list中的chunk最小使用比例
private final int minUsage;
// 當前list中的chunk最大使用比例
private final int maxUsage;
// 當前List中的chunk最大分配空間
private final int maxCapacity;
//chunk的頭元素
private PoolChunk<T> head;
上面出現了nextList,prevList,除了chunk是被組織成一個list管理的,連list本身也組成了一個更大的list。 爲什麼這麼做了,先不急,我們先簡單的看下PoolChunkList中的方法:
// 爲buf分配指定大小的內存
boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
// 如果list中沒有chunk則直接返回
if (head == null) {
return false;
}
// 我們一個一個chunk開始找
for (PoolChunk<T> cur = head;;) {
long handle = cur.allocate(normCapacity);
if (handle < 0) {
// handle < 0表示分配失敗,繼續到下一個chunk嘗試,一直到最後一個。
cur = cur.next;
if (cur == null) {
return false;
}
} else {
// 分配成功則將分配到的資源賦給ByteBuf
cur.initBuf(buf, handle, reqCapacity);
// 當前chunk的使用量超過一個上限閾值,則將其從當前list轉移到下一個list
if (cur.usage() >= maxUsage) {
remove(cur);
nextList.add(cur);
}
return true;
}
}
}
這段代碼有一處比較重要,當一個chunk的用量超過一定的比例,會將該chunk從當前list挪到下一個list中,這樣挪有什麼好處呢? 我們知道chunk本身是從連續的內存中分配一小段連續的內存,這樣實際使用內存者讀寫很方便,然而這種策略也帶來了一個壞處,隨着內存的不斷分配和回收,chunk中可能存在很多碎片。 碎片越來越多後我們想分配一段連續內存的失敗機率就會提高。針對這種情況我們可以把使用比例較大的chunk放到更後面,而先從使用比例更小的chunk中更早,這樣成功的機率就提高了。然而光把chunk往後放是不科學的,因爲隨着內存的釋放,原先被嚴重瓜分的chunk中會存在越來越多的大塊連續內存,所以還得在特定條件下把chunk從後往前調。調整的時機當然就是在內存釋放的時候了:
boolean free(PoolChunk<T> chunk, long handle) {
//釋放空間
chunk.free(handle);
//如果chunk釋放後的使用率小於當前list的閾值
if (chunk.usage() < minUsage) {
//從當前list中移除
remove(chunk);
// Move the PoolChunk down the PoolChunkList linked-list.
//把chunk往其它list轉移
return move0(chunk);
}
return true;
}
private boolean move0(PoolChunk<T> chunk) {
if (prevList == null) {
// There is no previous PoolChunkList so return false which result in having the PoolChunk destroyed and
// all memory associated with the PoolChunk will be released.
assert chunk.usage() == 0;
//如果沒有前一個list,則返回false,意味着會被釋放
return false;
}
//調用前一個list的move方法
return prevList.move(chunk);
}
private boolean move(PoolChunk<T> chunk) {
assert chunk.usage() < maxUsage;
//如果前一個list打不到要求則繼續往前移動
if (chunk.usage() < minUsage) {
// Move the PoolChunk down the PoolChunkList linked-list.
return move0(chunk);
}
//達到要求把它加入到當前list
// PoolChunk fits into this PoolChunkList, adding it here.
add0(chunk);
return true;
}
看完上面兩個方法,我們會發現一個chunk的生命週期並不是在一個固定的list中的,隨着內存的分配和釋放,他也會進入到不同的list中去。這樣我們就必須得注意,兩個相鄰的PoolChunkList,前一個list的maxUsage和後一個list的minUsage的值必須得有一段交叉得值來緩衝,否則會出現某個usage在臨界值的chunk不停的在兩個list之間來回移動。比如前一個list是【0,50】則後一個list可以是【25-75】而不能是【50-75】。同時也要注意尾節點上的maxUsage一定要等於100,這樣chunk佔滿後纔不會被繼續往後挪(後面也沒有可用list了)。
其它方法源碼分析
計算當前List中Chunk還可以分配的字節空間大小。
private static int calculateMaxCapacity(int minUsage, int chunkSize) {
minUsage = max(1, minUsage);
//如果最低使用率是百分百,那麼PoolChunk用了達到100%時會加入當前List,所以這個list已經沒有任何分配空間。
if (minUsage == 100) {
// If the minUsage is 100 we can not allocate anything out of this list.
return 0;
}
// Calculate the maximum amount of bytes that can be allocated from a PoolChunk in this PoolChunkList.
//
// As an example:
// - If a PoolChunkList has minUsage == 25 we are allowed to allocate at most 75% of the chunkSize because
// this is the maximum amount available in any PoolChunk in this PoolChunkList.
//計算可以分配的字節數量
//比如最低使用率是25,那麼PoolChunk在使用25%後會加入到此List,那麼可用空間就是 chunkSize * (1-25%)
return (int) (chunkSize * (100L - minUsage) / 100L);
}
//把chunk先當前list中移除
private void remove(PoolChunk<T> cur) {
//如果cur是head,這把head指向下一個元素
// x(head) -> y -> z
// y(head) -> z
if (cur == head) {
head = cur.next;
if (head != null) {
head.prev = null;
}
} else {
//如果cur不是head,這把自己去掉,使左右元素相連
//x(head) -> y(this) ->z
//x(head) -> z
PoolChunk<T> next = cur.next;
cur.prev.next = next;
if (next != null) {
next.prev = cur.prev;
}
}
}
//把chunk加入到當前list
void add(PoolChunk<T> chunk) {
//判斷如果chunk的使用率已經超過當前list的閾值,則加入到下一個list
if (chunk.usage() >= maxUsage) {
nextList.add(chunk);
return;
}
chunk.parent = this;
//如果head爲null,把chunk放第一個位置
if (head == null) {
head = chunk;
chunk.prev = null;
chunk.next = null;
} else {
//把chunk放入第一個位置
//head - x - y - z
//this(head) - 舊head - x - y -z
chunk.prev = null;
chunk.next = head;
head.prev = chunk;
head = chunk;
}
}