之前做了那麼多鋪墊,在這篇博文,我們將看清netty內存池管理的全貌。
PoolArena是一個抽象類,其子類爲HeapArena和DirectArena對應堆內存(heap buffer)和堆外直接內存(direct buffer),除了操作的內存(byte[]和ByteBuffer)不同外兩個類完全一致)。PoolArena管理了之前一系列的類,這裏講介紹它的實現細節。該類的實現接口是PoolArenaMetric,是一些信息的統計分析,我們暫時忽略。
下面來看下PoolArena的成員
static final int numTinySubpagePools = 512 >>> 4;
final PooledByteBufAllocator parent;
private final int maxOrder;// chunk相關滿二叉樹的高度
final int pageSize;// 單個page的大小
final int pageShifts;// 用於輔助計算
final int chunkSize;// chunk的大小
final int subpageOverflowMask; // 用於判斷請求是否爲Small/Tiny
final int numSmallSubpagePools;// small請求的雙向鏈表頭個數
final int directMemoryCacheAlignment;// 對齊基準
final int directMemoryCacheAlignmentMask;// 用於對齊內存
private final PoolSubpage<T>[] tinySubpagePools;// Subpage雙向鏈表
private final PoolSubpage<T>[] smallSubpagePools;// Subpage雙向鏈表
還有一些由PoolChunkList爲節點組成鏈表
private final PoolChunkList<T> q050;
private final PoolChunkList<T> q025;
private final PoolChunkList<T> q000;
private final PoolChunkList<T> qInit;
private final PoolChunkList<T> q075;
private final PoolChunkList<T> q100;
其中出現了tiny/small有必要解釋下,一圖勝千言,下面是我從網上找的圖片
即不同大小的內存塊,別叫不同的名稱,用於Chunk塊中的是Normal,正好8k爲1page,於是小於8k的內存塊成爲Tiny/Small,其中小於512B的爲Tiny。同理,Chunk塊存不下的內存塊爲Huge。
enum SizeClass {
Tiny,
Small,
Normal
// 除此之外的請求爲Huge
}
我們看下PoolArena的構造,構造挺多,我們一點一點分析。
this.parent = parent;
this.pageSize = pageSize;
this.maxOrder = maxOrder;
this.pageShifts = pageShifts;
this.chunkSize = chunkSize;
directMemoryCacheAlignment = cacheAlignment;
directMemoryCacheAlignmentMask = cacheAlignment - 1;
subpageOverflowMask = ~(pageSize - 1);
以上無非是成員賦初值。 tinySubpagePools = newSubpagePoolArray(numTinySubpagePools);
for (int i = 0; i < tinySubpagePools.length; i ++) {
tinySubpagePools[i] = newSubpagePoolHead(pageSize);
}
numSmallSubpagePools = pageShifts - 9;
smallSubpagePools = newSubpagePoolArray(numSmallSubpagePools);
for (int i = 0; i < smallSubpagePools.length; i ++) {
smallSubpagePools[i] = newSubpagePoolHead(pageSize);
}
對tiny/smallSubpagePool的初始化,跟我們之前分析的subpage關聯起來了。
q100 = new PoolChunkList<T>(this, null, 100, Integer.MAX_VALUE, chunkSize);
q075 = new PoolChunkList<T>(this, q100, 75, 100, chunkSize);
q050 = new PoolChunkList<T>(this, q075, 50, 100, chunkSize);
q025 = new PoolChunkList<T>(this, q050, 25, 75, chunkSize);
q000 = new PoolChunkList<T>(this, q025, 1, 50, chunkSize);
qInit = new PoolChunkList<T>(this, q000, Integer.MIN_VALUE, 25, chunkSize);
q100.prevList(q075);
q075.prevList(q050);
q050.prevList(q025);
q025.prevList(q000);
q000.prevList(null);
qInit.prevList(qInit);
這幾個PoolChunkList的命名其實是有含義的。其實是按照內存的使用率來取名的,如qInit代表一個chunk最開始分配後會進入它,隨着其使用率增大會逐漸從q000到q100,而隨着內存釋放,使用率減小,它又會慢慢的從q100到q00。我們再來看下錶
狀態 | 最小內存使用率 | 最大內存使用率 |
---|---|---|
QINIT | 1 | 25 |
Q0 | 1 | 50 |
Q25 | 25 | 75 |
Q50 | 50 | 100 |
Q75 | 75 | 100 |
Q100 | 100 | 100 |
也就是說,一條PoolChunkList對應上面相應的參數,其中的chunk使用率均符合其中的標準,否則會自動調整到相應的鏈中。
我們看下構造PoolChunkList鏈的方法。
void prevList(PoolChunkList<T> prevList) {
assert this.prevList == null;
this.prevList = prevList;
}
看上面的構造函數我們可以看出PoolChunkList爲節點的鏈的樣子
我們可以看到,如果chunk在Q25,當他使用率低於25則跑到Q0,再當他使用率爲0於是不再保留在內存中,其分配的內存被完全回收(它沒有前項指針)。再看看QInit,即使完全回收也不會被釋放,這樣始終保留在內存中(它前項指針指向自己),後面的分配就無需新建chunk,減小了分配的時間。
分配內存時先從內存佔用率相對較低的chunklist中開始查找,這樣查找的平均用時就會更短
private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
if (q050.allocate(buf, reqCapacity, normCapacity) || q025.allocate(buf, reqCapacity, normCapacity) ||
q000.allocate(buf, reqCapacity, normCapacity) || qInit.allocate(buf, reqCapacity, normCapacity) ||
q075.allocate(buf, reqCapacity, normCapacity)) {
return;
}
// Add a new chunk.
PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
long handle = c.allocate(normCapacity);
assert handle > 0;
c.initBuf(buf, handle, reqCapacity);
qInit.add(c);
}
源碼註釋告訴我們需要注意,上面這個方法已經是被synchronized修飾的了,因爲chunk本身的訪問不是線程安全的,因此我們在實際分配內存的時候必須保證線程安全,防止同一個內存塊被多個對象申請到。在這個方法中我們能看到,分配內存時的查找順序,先從低的開始找,但爲什麼不從q000開始?(網上找的答案,分析的非常到位!!)
在分析PoolChunkList的時候,我們知道一個chunk隨着內存的不停釋放,它本身會不停的往其所在的chunk list的prev list移動,直到其完全釋放後被回收。 如果這裏是從q000開始嘗試分配,雖然分配的速度可能更快了(因爲分配成功的機率更大),但一個chunk在使用率爲25%以內時有更大機率再分配,也就是一個chunk被回收的機率大大降低了。這樣就帶來了一個問題,我們的應用在實際運行過程中會存在一個訪問高峯期,這個時候內存的佔用量會是平時的幾倍,因此會多分配幾倍的chunk出來,而等高峯期過去以後,由於chunk被回收的機率降低,內存回收的進度就會很慢(因爲沒被完全釋放,所以無法回收),內存就存在很大的浪費。
爲什麼是從q050開始嘗試分配呢,q050是內存佔用50%~100%的chunk,猜測是希望能夠提高整個應用的內存使用率,因爲這樣大部分情況下會使用q050的內存,這樣在內存使用不是很多的情況下一些利用率低(<50%)的chunk慢慢就會淘汰出去,最終被回收。然而爲什麼不是從qinit中開始呢,這裏的chunk利用率低,但又不會被回收,豈不是浪費?q075,q100由於使用率高,分配成功的機率也會更小,因此放到最後(q100上的chunk使用率都是100%,爲什麼還要嘗試從這裏分配呢??)。
再往下,如果整個list都無法分配,創建一個新的chunk,加入到qinit中並分配空間。
我們再來順便看下huge的內存分配
private void allocateHuge(PooledByteBuf<T> buf, int reqCapacity) {
PoolChunk<T> chunk = newUnpooledChunk(reqCapacity);
activeBytesHuge.add(chunk.chunkSize());
buf.initUnpooled(chunk, reqCapacity);
allocationsHuge.increment();
}
直接使用了buf.initUnpooled(chunk, reqCapacity);沒用什麼優化策略,可能由於使用率不高。
是不是期待很久了,我們看下整內存分配的個過程吧
private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
final int normCapacity = normalizeCapacity(reqCapacity);
if (isTinyOrSmall(normCapacity)) { // capacity < pageSize
int tableIdx;
PoolSubpage<T>[] table;
boolean tiny = isTiny(normCapacity);
if (tiny) { // < 512
if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
// was able to allocate out of the cache so move on
return;
}
tableIdx = tinyIdx(normCapacity);
table = tinySubpagePools;
} else {
if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {
// was able to allocate out of the cache so move on
return;
}
tableIdx = smallIdx(normCapacity);
table = smallSubpagePools;
}
final PoolSubpage<T> head = table[tableIdx];
/**
* Synchronize on the head. This is needed as {@link PoolChunk#allocateSubpage(int)} and
* {@link PoolChunk#free(long)} may modify the doubly linked list as well.
*/
synchronized (head) {
final PoolSubpage<T> s = head.next;
if (s != head) {
assert s.doNotDestroy && s.elemSize == normCapacity;
long handle = s.allocate();
assert handle >= 0;
s.chunk.initBufWithSubpage(buf, handle, reqCapacity);
incTinySmallAllocation(tiny);
return;
}
}
synchronized (this) {
allocateNormal(buf, reqCapacity, normCapacity);
}
incTinySmallAllocation(tiny);
return;
}
if (normCapacity <= chunkSize) {
if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
// was able to allocate out of the cache so move on
return;
}
synchronized (this) {
allocateNormal(buf, reqCapacity, normCapacity);
++allocationsNormal;
}
} else {
// Huge allocations are never served via the cache so just call allocateHuge
allocateHuge(buf, reqCapacity);
}
}
終於把整個過程貼出來了,讓我們一點一點分析。先根據申請內存大小區分開來
1. tiny/small內存的話將table賦值爲tiny/smallsubPagePool。先從cache中獲取內存,失敗了則去對應的poolsubPage中去獲取(比如size爲16,則從tinylsubPagePool[0]中獲取,size爲512,則從smallsubPagePool[0]中獲取,以此類推),需要加鎖;如果雙向鏈表還沒初始化,則會使用Normal請求分配Chunk塊中的一個Page,Page以請求大小爲基準進行切分並分配第一塊內存,然後加入到雙向鏈表中(調用順序arena->chunkList->chunk->subpage)。
2.normal內存,先從cache中獲取,如果沒有則調用allocateNormal分配滿足要求的連續的Page塊。
3.對於Huge請求,則直接使用Unpooled直接分配。
其中內存大小類型有巧妙的位運算,可以看一下
// capacity < pageSize
boolean isTinyOrSmall(int normCapacity) {
return (normCapacity & subpageOverflowMask) == 0;
}
// normCapacity < 512
static boolean isTiny(int normCapacity) {
return (normCapacity & 0xFFFFFE00) == 0;
}
下面來看下內存釋放的整個過程
void free(PoolChunk<T> chunk, long handle, int normCapacity, PoolThreadCache cache) {
if (chunk.unpooled) {
int size = chunk.chunkSize();
destroyChunk(chunk);
activeBytesHuge.add(-size);
deallocationsHuge.increment();
} else {
SizeClass sizeClass = sizeClass(normCapacity);
if (cache != null && cache.add(this, chunk, handle, normCapacity, sizeClass)) {
// cached so not free it.
return;
}
freeChunk(chunk, handle, sizeClass);
}
}
如果內存是Huge類型,則直接釋放(調用抽象方法子類具體實現),並統計相關信息。否則,找出類型,並且可以緩存的話就緩存,否則釋放(調用freeChunk)。
void freeChunk(PoolChunk<T> chunk, long handle, SizeClass sizeClass) {
final boolean destroyChunk;
synchronized (this) {
switch (sizeClass) {
case Normal:
++deallocationsNormal;
break;
case Small:
++deallocationsSmall;
break;
case Tiny:
++deallocationsTiny;
break;
default:
throw new Error();
}
destroyChunk = !chunk.parent.free(chunk, handle);
}
if (destroyChunk) {
// destroyChunk not need to be called while holding the synchronized lock.
destroyChunk(chunk);
}
}
其中parent是poolChunkList,free則是先釋放handle空間,再從對應的qXXX不斷內存裝填->q000最後如果有多出的chunk則,調用抽象方法destroy(chunk);(具體子類來實現)
可以注意到本類重寫了Object的finalize()方法,該可能會在方法對象被在gc前調用
@Override
protected final void finalize() throws Throwable {
try {
super.finalize();
} finally {
destroyPoolSubPages(smallSubpagePools);
destroyPoolSubPages(tinySubpagePools);
destroyPoolChunkLists(qInit, q000, q025, q050, q075, q100);
}
}
private static void destroyPoolSubPages(PoolSubpage<?>[] pages) {
for (PoolSubpage<?> page : pages) {
page.destroy();
}
}
private void destroyPoolChunkLists(PoolChunkList<T>... chunkLists) {
for (PoolChunkList<T> chunkList: chunkLists) {
chunkList.destroy(this);
}
}
本類還一個值得一看的方法,重新分配內存
void reallocate(PooledByteBuf<T> buf, int newCapacity, boolean freeOldMemory) {
if (newCapacity < 0 || newCapacity > buf.maxCapacity()) {
throw new IllegalArgumentException("newCapacity: " + newCapacity);
}
int oldCapacity = buf.length;
if (oldCapacity == newCapacity) {
return;
}
PoolChunk<T> oldChunk = buf.chunk;
long oldHandle = buf.handle;
T oldMemory = buf.memory;
int oldOffset = buf.offset;
int oldMaxLength = buf.maxLength;
int readerIndex = buf.readerIndex();
int writerIndex = buf.writerIndex();
allocate(parent.threadCache(), buf, newCapacity);
if (newCapacity > oldCapacity) {
memoryCopy(
oldMemory, oldOffset,
buf.memory, buf.offset, oldCapacity);
} else if (newCapacity < oldCapacity) {
if (readerIndex < newCapacity) {
if (writerIndex > newCapacity) {
writerIndex = newCapacity;
}
memoryCopy(
oldMemory, oldOffset + readerIndex,
buf.memory, buf.offset + readerIndex, writerIndex - readerIndex);
} else {
readerIndex = writerIndex = newCapacity;
}
}
buf.setIndex(readerIndex, writerIndex);
if (freeOldMemory) {
free(oldChunk, oldHandle, oldMaxLength, buf.cache);
}
}
一點一點分析,如果重新分配內存跟原內存大小一致,那麼直接返回。先重新申請一段所需大小的空間,如果原來申請的內存大小小於新申請的,那麼把原內存的內容拷貝到新內存中,否則,把原內存可讀的部分數據拷貝過來,但如果連可讀的數據大小都比新申請的內存大小要大,那麼沒可讀的內存了。設置好readIndex跟writeIndex,然後把原內存釋放。
有沒有發現到這裏,整個內存池的過程基本上理清了,Arene->chunkList->chunk,Arena->subPage。
剩下cache裏面的細節跟往外PooledByteBuf的細節(有沒有發現這個已經接觸到了buf,快到使用層面了,read/writeIndex也是很眼熟的吧)是我們沒梳理的。
最後列舉下剩下的抽象方法:
// 判斷子類實現Heap還是Direct
abstract boolean isDirect();
// 新建一個Chunk,Tiny/Small,Normal請求請求分配時調用
protected abstract PoolChunk<T> newChunk(int pageSize, int maxOrder, int pageShifts, int chunkSize);
// 新建一個Chunk,Huge請求分配時調用
protected abstract PoolChunk<T> newUnpooledChunk(int capacity);
protected abstract PooledByteBuf<T> newByteBuf(int maxCapacity);
// 複製內存,當ByteBuf擴充容量時調用
protected abstract void memoryCopy(T src, int srcOffset, T dst, int dstOffset, int length);
// 銷燬Chunk,釋放內存時調用
protected abstract void destroyChunk(PoolChunk<T> chunk);