1.前言
之前的章節已經將啟動demo中能看見的內容都分析完了,Netty的一個整體樣貌都在第8節線程模型最后給的圖畫出來了。這些內容解釋了Netty為什么是一個異步事件驅動的程序,也解釋了Netty的線程模型的高效,但是並沒有涉及到的一個方面就是Handler的解析過程。通過前面的知識點我們都應該明白了Handler用於對獲取的數據按照相關協議進行解析,Java的NIO都是通過buffer完成的讀寫的,這里關於Netty的另一個高效性卻沒有涉及,那就是內存管理,這個階段發生在handler讀取數據的階段。
Netty使用了什么方式管理內存的?為什么需要內存管理?第一個問題是本節的主要內容,第二個問題在此給出答案。原因在於IO操作涉及到頻繁的內存分配銷毀,如果是在堆上分配內存空間,將會使得GC操作非常頻繁,這對性能有極大的損耗,所以Netty使用了JDK1.5提供的堆外內存分配,程序可以直接操作不歸JVM管轄的內存空間,但是需要自己控制內存的創建和銷毀。通過堆外內存的方式,避免了頻繁的GC,但是帶來了另外一個問題堆外內存創建的效率十分的低,所以頻繁創建堆外內存更加糟糕。基於上述原因,Netty最終設計了一個堆外內存池,申請了一大塊內存空間,然后對這塊內存空間提供管理接口,讓應用層不需要關注內存操作,能夠直接拿到相關數據。但是Netty並沒有完全放棄在堆上開辟內存,提供了相應的接口。
要知道Netty是如何管理內存的,還需要搞明白幾件事情。
final ByteBufAllocator allocator = config.getAllocator(); final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle(); byteBuf = allocHandle.allocate(allocator); allocHandle.lastBytesRead(doReadBytes(byteBuf));
這個是NioByteUnsafe的read方法中的一段代碼,可以看出handler的一個read基本思路:獲取ByteBufAllocator,再獲取RecvByteBufAllocator.Handle,通過這兩個類獲取一個ByteBuf,最后將數據寫入這個ByteBuf中。這個步驟就說明了一些讀取數據的相關概念。再了解一下Nio的Allocator是DefaultChannelConfig的allocator字段,最終生成在ByteBufUtil的static方法中。其根據io.netty.allocator.type參數來決定使用哪種類型的,默認有UnpooledByteBufAllocator和PooledByteBufAllocator兩種,即使用內存池或是不使用,Android平台默認unpooled,其它平台默認pooled。Nio默認的RecvByteBufAllocator則是AdaptiveRecvByteBufAllocator,這個設置在DefaultChannelConfig的構造方法里。了解了這些我們接下來分別看這三個類的體系結構。
2 ByteBufAllocator
該接口實例必須在線程安全的環境下使用。該接口定義的方法分為下面幾種:
buffer():分配一個ByteBuf,是direct(直接內存緩存)還是heap(堆內存緩存)由具體實現決定。
ioBuffer():分配一個ByteBuf,最好是適合IO操作的direct內存緩存。
heapBuffer():分配一個堆內存緩存
directBuffer():分配一個direct直接內存緩存
compositeBuffer():分配一個composite緩存,是direct還是heap由具體實現決定
compositeHeapBuffer():分配一個composite的堆緩存
compositeDirectBuffer():分配一個composite的直接緩存
isDirectBufferPooled():是否是direct緩存池
calculateNewCapacity():計算ByteBuffer需要擴展時使用的容量
根據接口方法我們可以很清楚ByteBuf分為direct和heap兩種類型,又因為分為pool和unpool,所以笛卡兒積就是4中類型了。composite類型的定義將在ByteBuf時講解。
2.1 AbstractByteBufAllocator
抽象父類,定義了一些默認數據:1.默認緩存初始大小256,最大Integer.MAX_VALUE,components為16,threshold是1048576 * 4(4MB)。
抽象父類提供了包裝ByteBuf類成可以檢測內存泄漏的靜態方法toLeakAwareBuffer。其還通過構造方法的參數preferDirect決定上面接口定義有歧義的方法是使用direct還是buffer,為true再滿足平台條件就使用direct,否則就是heap。ioBuffer方法只需要滿足平台支持,就會優先使用direct,和方法描述一致,平台不支持就會使用heap。compositeBuffer相關方法都是直接創建了CompositeByteBuf對象,通過toLeakAwareBuffer方法包裝返回。calculateNewCapacity的邏輯是如果minNewCapacity等於threshold,就直接返回。大於就返回增加threshold的數值。小於從64開始*2,直到超過minNewCapacity。
最終抽象父類包裝完了基本的方法,只剩下newHeapBuffer和newDirectBuffer方法交給子類來實現了。
2.2 PooledByteBufAllocator
該類構造參數preferDirect為false,所以其更傾向於使用heap內存,當然具體看是使用的哪個方法了。默認的pageSize是8192,maxOrder是11,chunkSize是8192<<11,tinyCacheSize是512,smallCacheSize是256,normalCacheSize是64,對這些參數有所疑惑是正常的,具體可以先看下Netty的內存管理文章:這里。簡單看下就能明白這些參數的含義了。
protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
PoolThreadCache cache = threadCache.get();
PoolArena<byte[]> heapArena = cache.heapArena;
final ByteBuf buf;
if (heapArena != null) {
buf = heapArena.allocate(cache, initialCapacity, maxCapacity);
} else {
buf = PlatformDependent.hasUnsafe() ?
new UnpooledUnsafeHeapByteBuf(this, initialCapacity, maxCapacity) :
new UnpooledHeapByteBuf(this, initialCapacity, maxCapacity);
}
return toLeakAwareBuffer(buf);
}
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
PoolThreadCache cache = threadCache.get();
PoolArena<ByteBuffer> directArena = cache.directArena;
final ByteBuf buf;
if (directArena != null) {
buf = directArena.allocate(cache, initialCapacity, maxCapacity);
} else {
buf = PlatformDependent.hasUnsafe() ?
UnsafeByteBufUtil.newUnsafeDirectByteBuf(this, initialCapacity, maxCapacity) :
new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
}
return toLeakAwareBuffer(buf);
}
上面是實現抽象父類未實現的內容。可以看到都是分為幾步走:1.獲取threadCache,線程本地緩存。2.獲取對應的PoolArena,heap的是byte[]對象,direct的是ByteBuffer對象。3.存在就分配一個,不存在就創建一個。4.最后都通過toLeakAwareBuffer包裝成內存泄漏檢測的buffer。
threadCache就表面這個緩存是綁定了線程的,所以之前接口上就說明了必須保證使用時的線程安全。
2.3 UnpooledByteBufAllocator
未使用池技術的沒有過多可講解的內容,不是本章重點。
protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
return PlatformDependent.hasUnsafe() ?
new InstrumentedUnpooledUnsafeHeapByteBuf(this, initialCapacity, maxCapacity) :
new InstrumentedUnpooledHeapByteBuf(this, initialCapacity, maxCapacity);
}
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
final ByteBuf buf;
if (PlatformDependent.hasUnsafe()) {
buf = noCleaner ? new InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf(this, initialCapacity, maxCapacity) :
new InstrumentedUnpooledUnsafeDirectByteBuf(this, initialCapacity, maxCapacity);
} else {
buf = new InstrumentedUnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
}
return disableLeakDetector ? buf : toLeakAwareBuffer(buf);
}
都是直接返回的,不做特別的說明。
3 RecvByteBufAllocator
該接口就一個方法,生成一個Handler。

Handler已經被ExtendedHandler取代了,不過也是直接繼承了Handler。allocate提供了分配方法,handler就是來管理如何分配。
allocate():分配一個ByteBuf,要足夠大獲取所有輸入數據,也要足夠小不要浪費太多空間。
guess():猜測allocate應該要分配多大的空間
reset():重置累積的計數,並建議在下一個讀loop要讀取多少字節或者消息。應該使用continueReading方法判斷讀操作是否結束
incMessagesRead():增加本次讀loop的讀取message的計數
lastBytesRead():設置最后一次讀操作獲取的字節
attemptedBytesRead():設置讀操作需要嘗試讀取的字節數
continueReading():決定當前讀loop是否繼續
readComplete():讀操作已完成。
3.1 AdaptiveRecvByteBufAllocator
該類也設置了一些默認參數,最小大小為64,最大為65536,初始化大小為1024,索引增長步長為4,減少為1。
static {
List<Integer> sizeTable = new ArrayList<Integer>();
for (int i = 16; i < 512; i += 16) {
sizeTable.add(i);
}
for (int i = 512; i > 0; i <<= 1) {
sizeTable.add(i);
}
SIZE_TABLE = new int[sizeTable.size()];
for (int i = 0; i < SIZE_TABLE.length; i ++) {
SIZE_TABLE[i] = sizeTable.get(i);
}
}
靜態方法初始化了一個SIZE_TABLE字段,類型為int[]。可以從這段邏輯看出改字段存儲了:16,32,48,64...496,512,1024,2048....2^31。這些元素。這些字段有什么含義呢?具體看使用了這個表的方法:
private static int getSizeTableIndex(final int size) {
for (int low = 0, high = SIZE_TABLE.length - 1;;) {
if (high < low) {
return low;
}
if (high == low) {
return high;
}
int mid = low + high >>> 1;
int a = SIZE_TABLE[mid];
int b = SIZE_TABLE[mid + 1];
if (size > b) {
low = mid + 1;
} else if (size < a) {
high = mid - 1;
} else if (size == a) {
return mid;
} else {
return mid + 1;
}
}
}
這個方法名就說明了其作用是獲取表的下標索引,這大體上是個二分查找的思路。給了一個初始大小,如果該值比中間值右邊還大,繼續,low重新設置;比中間值小,也繼續,high重新設置。如果等於中間值返回下標,如果介於中間值和中間值大一個的位置,返回中間值大一個的內容。
這么理解不好理解,再看上面的接口定義的allocate的方法,目標是不要浪費太多空間,也要滿足能夠讀取所有的內容。有了這么個前提這個操作就好理解了,這是開發人員根據實際設計的一組空間分配的大小設置,根據size,來獲取一個合適的分配大小。這個方法就是獲取合適大小的表大小數組的下標。所以介於中間值和大於中間值的位置,就返回了大於中間值的位置,保證這兩個內容,足夠,浪費最小。至於為什么前面都是16累計,到了512就變成了*2累計,這個估計和實際情況相關而設計的。
3.2 HandleImpl
這個就是AdaptiveRecvByteBufAllocator提供的一個Handler了,回到最初給的代碼,通過recvallocator的handler,處理allocator,最終獲取讀取的buffer。ExtendedHandle定義的11個方法,這里只實現了4個,其它的由父類MaxMessageHandle實現了。先看MaxMessageHandler類的相關內容。

maxMessagePerRead:一次讀取最多的數據量
totalMessages:總共的消息量
totalBytesRead:總共的字節量(前面也提到過,分為message和byte)
attemptedBytesRead:嘗試讀取的字節數
lastBytesRead:最后讀取的字節數
大部分方法都是直接返回相關的字段,其它內容不值一提,需要結合實際使用進行分析。allocate方法,實際上調用了alloc的ioBuffer方法,即盡可能的使用direct模式。
HandleImpl實際也沒有太多內容,最難理解的就是上面說的獲取大小適宜的緩沖區的計算需要大小的步驟了。
小結一下:RecvByteBufAllocator的目的就一個,為了開辟合適大小的緩沖區。
4. ByteBuf
此概念是最重要又不重要的一個環節。ByteBuf抽象類實現了ReferenceCounted,這個是Netty設計的direct內存一個重點。上面談到過heap模式存在頻繁的GC,direct模式如果頻繁開辟緩存和銷毀,性能更低,所以采取了Pool的方式管理direct。而實際上使用池的技術也需要標記已使用的,和未使用的區域,使用完成之后也需要進行釋放。Netty采取了一種GC策略,引用計數法。有一個類引用了該Buffer,+1,release的時候-1。為0的時候就都不使用了,這個時候該區域就可以進行釋放。這個就是ReferenceCounted所做的事情,引用計數。

refCnt:當前計數,為0意味着收回
retain:引用+1,或者+指定數值
touch:調試使用,記錄當前訪問該對象的位置。ResourceLeakDetector可以提供相關信息。
release:引用-1,或者-指定數值
抽象父類ByteBuf的方法很多,這里不進行截圖,簡單介紹說明(太多了,多數看名稱就能明白相關意思):
capacity():當前容量;傳入數值,小於當前容量截斷,大於擴容到這個值
maxCapacity():允許的最大容量
alloc():獲取開辟這個緩存的alloc對象
order():設置解析字節碼的字節順序,換另一種說法可能更清楚(Big-endian和Little-endian),具體見wiki:這里。該方法被廢棄,后面直接使用getShort和getShortLE進行區分。
unwarp():如果該類是包裝類,返回未包裝前的對象。
isDirect():是否是直接內存
isReadOnly():是否只讀
asReadOnly():返回一個只讀的ByteBuf版本
readerIndex():返回讀取下標,帶參數就是設置這個值
writerIndex():返回寫下標,帶參數就是設置
其它的一些方法有:判斷可讀可寫狀態,和相關字節數。清除內容,標記讀位置,重置讀位置,標記寫和重置寫。丟棄一些讀取字節。然后就是基本的讀取int,short等基本類型的方法,這里之前也說過多了一個LE方法。跳過一些字節。拷貝緩存,返回可讀的字節片段,復制字節,內存地址等一系列方法。
ByteBuf的相關方法定義實際上和JAVA提供的ByteBuffer區別不是很大,保持了一致性。下面我們只介紹兩種ByteBuf。
4.1 PooledUnsafeDirectByteBuf
這種buf可能就是我們想要研究的了,從最上層的抽象父類一層層往下看其相關操作。
1.AbstractByteBuf:

該抽象類解決了基本的方法:可讀可寫的相關判斷。清除,標記讀寫位置,丟棄字節。其他的方法大體都交給子類實現了,父類只是確定了一下安全性,比如getInt是不是有2個可讀字節之類的。
2.AbstractReferenceCountedByteBuf:

該抽象父類最大的作用就是完成了之前引用計數的問題,通過CAS操作更新。相關方法的實現。
3.PooledByteBuf:這個方法就是相關內存池的操作。

這個類就實現了一些基本的capacity、slice、duplicate、deallocate的相關方法,基本上是通過上圖的參數實現的。
最后就是我們要說的PooledUnsafeDirectByteBuf類了,該類都是通過UnsafeByteBufUtil類來實現相關方法。實際上看過去這些方法都不復雜,因為復雜的部分被簡單略過了,比如分配緩存的時候具體是如何操作的,那個是在Pool方法中,使用緩存的方法倒是不復雜,但是有兩個內容也忽略了,一個是leakDetector另一個就是recycler了。這些都不進行介紹。
4.2 CompositeByteBuf
這個類是之前沒有介紹清楚的,組合型ByteBuf,到底是什么含義呢?其實該類與Netty中的零拷貝有關。這個零拷貝和最初節所說的操作系統層次的零拷貝不是一個概念,Netty的零拷貝指的是用戶態之間的內存拷貝。即減少從用戶地址的一個位置拷貝到另一個地址的次數。比如說文件FileRegion,可以通過transferTo方法將文件緩沖直接交給Channel,而不用通過while循環獲取數據,再傳遞。又比如一個消息由多個模塊組成,比如請求頭,請求體。通常要合並成一個Buffer,這個就產生了在用戶態中進行拷貝數據了。這個解決方法也就是現在所說的CompositeByteBuf,組合ByteBuf。將組件添加到這個buffer中,再對操作層透明的傳輸這些數據。

components是一個ArrayList,元素就是ByteBuf,maxNumComponents就是最大的組件個數,默認16個。
private int addComponent0(boolean increaseWriterIndex, int cIndex, ByteBuf buffer) {
assert buffer != null;
boolean wasAdded = false;
try {
checkComponentIndex(cIndex);
int readableBytes = buffer.readableBytes();
// No need to consolidate - just add a component to the list.
@SuppressWarnings("deprecation")
Component c = new Component(buffer.order(ByteOrder.BIG_ENDIAN).slice());
if (cIndex == components.size()) {
wasAdded = components.add(c);
if (cIndex == 0) {
c.endOffset = readableBytes;
} else {
Component prev = components.get(cIndex - 1);
c.offset = prev.endOffset;
c.endOffset = c.offset + readableBytes;
}
} else {
components.add(cIndex, c);
wasAdded = true;
if (readableBytes != 0) {
updateComponentOffsets(cIndex);
}
}
if (increaseWriterIndex) {
writerIndex(writerIndex() + buffer.readableBytes());
}
return cIndex;
} finally {
if (!wasAdded) {
buffer.release();
}
}
}
private Component findComponent(int offset) {
checkIndex(offset);
for (int low = 0, high = components.size(); low <= high;) {
int mid = low + high >>> 1;
Component c = components.get(mid);
if (offset >= c.endOffset) {
low = mid + 1;
} else if (offset < c.offset) {
high = mid - 1;
} else {
assert c.length != 0;
return c;
}
}
throw new Error("should not reach here");
}
可以看見添加組件,和定位哪個組件的方法,這樣就簡單的實現了將多個ByteBuf合並,對外提供透明接口,不用具體開辟新的空間,再拷貝相關數據了。
5 后記
本節題目雖然叫做內存管理,但是核心內容較少,涉及算法,比較繁瑣。但大體介紹了一下Netty的內存設計思路,將了與之相關的類的實現。歸納如下:
1.Netty大致有4中內存方法,Heap Pool,Direct Pool, Heap unPool,Dircet unPool。
2.Pool和UnPool的大體區別
3.ByteBuf采取了引用計數的方式管理這塊被分配了的內存
4.Netty零拷貝實現之一CompositeByteBuf,將多個ByteBuf組合起來,提供操作一個的接口。
最重要的Pool方式的實現沒有說明,而且內存泄露檢測,recycle等內容也沒有說明。這些都需要自己去琢磨。再附上文中提到的一個其它博客對這塊算法的介紹:這里,作為這章內容的缺失補充。再補充一下Netty零拷貝的相關知識:這里。
最后再給出一個別人使用Netty進行優化的例子:這里,結合前面的知識,這個應該能夠看懂。另外,此系列基本就到此為止了,其它的應該不會再進行詳細介紹,后面可能會補充一些具體使用的demo。
