(二)Netty必學知識點 感悟優化——Netty對JDK緩衝區的記憶體池零拷貝改造
NIO中緩衝區是資料傳輸的基礎,JDK通過ByteBuffer實現,Netty框架中並未採用JDK原生的ByteBuffer,而是構造了ByteBuf。
ByteBuf對ByteBuffer做了大量的優化,比如說記憶體池,零拷貝,引用計數(不依賴GC),本文主要是分析這些優化,學習這些優化思想,學以致用,在實際工程中,借鑑這些優化方案和思想。
直接記憶體和堆記憶體
首先先講一下這裡面需要用的基礎知識,在JVM中 記憶體可分為兩大塊,一個是堆記憶體,一個是直接記憶體。這裡簡單介紹一下
堆記憶體:
堆記憶體是Jvm所管理的記憶體,相比方法區,棧記憶體,堆記憶體是最大的一塊。所有的物件例項例項以及陣列都要在堆上分配。
Java的垃圾收集器是可以在堆上回收垃圾。
直接記憶體:
JVM使用Native函式在堆外分配記憶體,之後通過Java堆中的DirectByteBuffer物件作為這塊記憶體的引用進行操作。直接記憶體不會受到Java堆的限制,只受本機記憶體影響。
Java的GC只會在老年區滿了觸發Full GC時,才會去順便清理直接記憶體的廢棄物件。
JDK原生緩衝區ByteBuffer
在NIO中,所有資料都是用緩衝區處理的。讀寫資料,都是在緩衝區中進行的。快取區實質是是一個數組,通常使用位元組緩衝區——ByteBuffer。
屬性:
使用方式:
ByteBuffer可以申請兩種方式的記憶體,分別為堆記憶體和直接記憶體,首先看申請堆記憶體。
// 申請堆記憶體 ByteBuffer HeapbyteBuffer = ByteBuffer.allocate(1024);
很簡單,就一行程式碼,再看看allocate方法。
public static ByteBuffer allocate(int capacity) { if (capacity < 0) throw new IllegalArgumentException(); return new HeapByteBuffer(capacity, capacity); }
其實就是new一個HeapByteBuffer物件。這個 HeapByteBuffer繼承自ByteBuffer,構造器採用了父類的構造器,如下所示:
HeapByteBuffer(int cap, int lim) { // package-private super(-1, 0, lim, cap, new byte[cap], 0); /* hb = new byte[cap]; offset = 0; */ }//ByteBuffer構造器 ByteBuffer(int mark, int pos, int lim, int cap, // package-private byte[] hb, int offset) { super(mark, pos, lim, cap); this.hb = hb; this.offset = offset; }
結合ByteBuffer的四個屬性,初始化的時候就可以賦值capaticy,limit,position,mark,至於byte[] hb, int offsef這兩個屬性,JDK文件給出的解釋是 backing array , and array offset 。它是一個回滾陣列,offset是陣列的偏移值。
申請直接記憶體:
// 申請直接記憶體 ByteBuffer DirectbyteBuffer = ByteBuffer.allocateDirect(1024);
allocateDirect()實際上就是new的一個DirectByteBuffer物件,不過這個new 一個普通物件不一樣。這裡使用了Native函式來申請記憶體,在Java中就是呼叫unsafe物件
public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); } DirectByteBuffer(int cap) { // package-private super(-1, 0, cap, cap); boolean pa = VM.isDirectMemoryPageAligned(); int ps = Bits.pageSize(); long size = Math.max(1L, (long)cap + (pa ? ps : 0)); Bits.reserveMemory(size, cap); long base = 0; try { base = unsafe.allocateMemory(size); } catch (OutOfMemoryError x) { Bits.unreserveMemory(size, cap); throw x; } unsafe.setMemory(base, size, (byte) 0); if (pa && (base % ps != 0)) { // Round up to page boundary address = base + ps - (base & (ps - 1)); } else { address = base; } cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); att = null; } View Code
申請方法不同的記憶體有不同的用法。接下來看一看ByteBuffer的常用方法與如何使用
ByteBuffer的常用方法與使用方式
Bytebuf的讀和寫是使用put()和get()方法實現的
// 讀操作public byte get() { return hb[ix(nextGetIndex())]; }final int nextGetIndex() { if (position >= limit) throw new BufferUnderflowException(); return position++; }// 寫操作public ByteBuffer put(byte x) { hb[ix(nextPutIndex())] = x; return this; }final int nextPutIndex() { if (position >= limit) throw new BufferOverflowException(); return position++; }
從程式碼中可以看出,讀和寫操作都會改變ByteBuffer的position屬性,這兩個操作是共用的position屬性。這樣就會帶來一個問題,讀寫操作會導致資料出錯啊,資料位置出錯。
ByteBuffer提供了flip()方法,讀寫模式切換,切換的時候會改變position和limit的位置。看看flip()怎麼實現的:
public final Buffer flip() { // 1. 設定 limit 為當前位置 limit = position; // 2. 設定 position 為0 position = 0; mark = -1; return this; }
這裡就不重點介紹了,有些細節可以自己去深究。
Netty的ByteBuf
Netty使用的自身的ByteBuf物件來進行資料傳輸,本質上使用了外觀模式對JDK的ByteBuffer進行封裝。
相較於原生的ByteBuffer,Netty的ByteBuf做了很多優化,零拷貝,記憶體池加速,讀寫索引。
為什麼要使用記憶體池?
首先要明白一點,Netty的記憶體池是不依賴於JVM本身的GC的。
回顧一下直接記憶體的GC:
上文提到Java的GC只會在老年區滿了觸發Full GC時,才會去順便清理直接記憶體的廢棄物件。
JVM中的直接記憶體,存在堆記憶體中其實就是DirectByteBuffer類,它本身其實很小,真的記憶體是在堆外,這裡是對映關係。
每次申請直接記憶體,都先看看是否超限 —— 直接記憶體的限額預設(可用 -XX:MaxDirectMemorySize 重新設定)。
如果超過限額,就會主動執行System.gc(),這樣會帶來一個影響,系統會中斷100ms。如果沒有成功回收直接記憶體,並且還是超過直接記憶體的限額,就會丟擲OOM——記憶體溢位。
繼續從GC角度分析,DirectByteBuffer熬過了幾次young gc之後,會進入老年代。當老年代滿了之後,會觸發Full GC。
因為本身很小,很難佔滿老年代,因此基本不會觸發Full GC,帶來的後果是大量堆外記憶體一直佔著不放,無法進行記憶體回收。
還有最後一個辦法,就是依靠申請額度超限時觸發的system.gc(),但是前面提到,它會中斷程序100ms,如果在這100ms的之間,系統未完成GC,仍會丟擲OOM。
所以這個最後一個辦法也不是完全保險的。
Netty使用了引用計數的方式,主動回收記憶體。回收的物件包括非池直接記憶體,和記憶體池中的記憶體。
記憶體池的記憶體洩露檢測?
Netty中使用引用計數機制來管理資源,ByteBuf實際上是實現了ReferenceCounted介面,當例項化ByteBuf物件時,引用計數加1。
當應用程式碼保持一個物件引用時,會呼叫retain方法將計數增加1,物件使用完畢進行釋放,呼叫release將計數器減1.
當引用計數變為0時,物件將釋放所有的資源,返回記憶體池。
Netty記憶體洩漏檢測級別:
禁用(DISABLED) - 完全禁止洩露檢測。不推薦。
簡單(SIMPLE) - 告訴我們取樣的1%的緩衝是否發生了洩露。預設。
高階(ADVANCED) - 告訴我們取樣的1%的緩衝發生洩露的地方
偏執(PARANOID) - 跟高階選項類似,但此選項檢測所有緩衝,而不僅僅是取樣的那1%。此選項在自動測試階段很有用。如果構建(build)輸出包含了LEAK,可認為構建失敗也可以使用JVM的-Dio.netty.leakDetectionLevel選項來指定洩漏檢測級別。
記憶體跟蹤
在記憶體池中分配記憶體,得到的ByteBuf物件都是經過 toLeakAwareBuffer()方法封裝的,該方法作用就是對ByteBuf物件進行引用計數,使用 SimpleLeakAwareByteBuf或者 AdvancedLeakAwareByteBuf 來包裝ByteBuf。此外該方法只對非池記憶體中的直接記憶體和記憶體池中的記憶體進行記憶體洩露檢測。
//裝飾器模式,用SimpleLeakAwareByteBuf或AdvancedLeakAwareByteBuf來包裝原始的
ByteBufprotected static ByteBuf toLeakAwareBuffer(ByteBuf buf) {
ResourceLeakTracker<ByteBuf> leak;
//根據設定的Level來選擇使用何種裝飾器
switch (ResourceLeakDetector.getLevel()) {
case SIMPLE:
//建立用於跟蹤和表示內容洩露的ResourcLeak物件
leak = AbstractByteBuf.leakDetector.track(buf);
if (leak != null) { //只在ByteBuf.order方法中呼叫
ResourceLeak.record
buf = new SimpleLeakAwareByteBuf(buf, leak);
}
break;
case ADVANCED:
case PARANOID:
leak = AbstractByteBuf.leakDetector.track(buf);
if (leak != null) { //只在ByteBuf.order方法中呼叫
ResourceLeak.record
buf = new AdvancedLeakAwareByteBuf(buf, leak);
}
break;
default:
break;
}
return buf;
}
實際上,記憶體洩露檢測是在 AbstractByteBuf.leakDetector.track(buf)進行的,來看看track方法的具體實現。
/** * Creates a new {@link ResourceLeakTracker} which is expected to be closed via * {@link ResourceLeakTracker#close(Object)} when the related resource is deallocated. * * @return the {@link ResourceLeakTracker} or {@code null} */
@SuppressWarnings("unchecked")
public final ResourceLeakTracker<T> track(T obj) {
return track0(obj);
}
@SuppressWarnings("unchecked")
private DefaultResourceLeak track0(T obj) {
Level level = ResourceLeakDetector.level; // 不進行記憶體跟蹤 if (level == Level.DISABLED) {
return null; }
if (level.ordinal() < Level.PARANOID.ordinal()) {
//如果監控級別低於PARANOID,在一定的取樣頻率下報告記憶體洩漏、
if ((PlatformDependent.threadLocalRandom().nextInt(samplingInterval)) == 0) { reportLeak();
return new DefaultResourceLeak(obj, refQueue, allLeaks);
}
return null;
}
//每次需要分配 ByteBuf 時,報告記憶體洩露情
reportLeak();
return new DefaultResourceLeak(obj, refQueue, allLeaks);
}
再來看看返回物件——DefaultResourceLeak,他的實現方式如下:
private static final class DefaultResourceLeak<T> extends WeakReference<Object> implements ResourceLeakTracker<T>, ResourceLeak {
它繼承了虛引用WeakReference,虛引用完全不影響目標物件的垃圾回收,但是會在目標物件被VM垃圾回收時加入到引用佇列,
正常情況下ResourceLeak物件,會將監控的資源的引用計數為0時被清理掉。
但是當資源的引用計數失常,ResourceLeak物件也會被加入到引用佇列.
存在著這樣一種情況:沒有成對呼叫ByteBuf的retain和relaease方法,導致ByteBuf沒有被正常釋放,當 ResourceLeak(引用佇列) 中存在元素時,即表明有記憶體洩露。
Netty中的 reportLeak()方法來報告記憶體洩露情況,通過檢查引用佇列來判斷是否有記憶體洩露,並報告跟蹤情況.
方法程式碼如下:
View Code
Handler中的記憶體處理機制
Netty中有handler鏈,訊息有本Handler傳到下一個Handler。所以Netty引入了一個規則,誰是最後使用者,誰負責釋放。
根據誰最後使用誰負責釋放的原則,每個Handler對訊息可能有三種處理方式
對原訊息不做處理,呼叫 ctx.fireChannelRead(msg)把原訊息往下傳,那不用做什麼釋放。
將原訊息轉化為新的訊息並呼叫 ctx.fireChannelRead(newMsg)往下傳,那必須把原訊息release掉。
如果已經不再呼叫ctx.fireChannelRead(msg)傳遞任何訊息,那更要把原訊息release掉。
假設每一個Handler都把訊息往下傳,Handler並也不知道誰是啟動Netty時所設定的Handler鏈的最後一員,所以Netty在Handler鏈的最末補了一個TailHandler,如果此時訊息仍然是ReferenceCounted型別就會被release掉。
總結:
1.Netty在不同的記憶體洩漏檢測級別情況下,取樣概率是不一樣的,在Simple情況下出現了Leak,要設定“-Dio.netty.leakDetectionLevel=advanced”再跑一次程式碼,找到建立和訪問的地方。
2.Netty中的記憶體洩露檢測是通過對ByteBuf物件進行裝飾,利用虛引用和引用計數來對非池中的直接記憶體和記憶體池中記憶體進行跟蹤,判斷是否發生記憶體洩露。
3.計數器基於 AtomicIntegerFieldUpdater,因為ByteBuf物件很多,如果都把int包一層AtomicInteger花銷較大,而AtomicIntegerFieldUpdater只需要一個全域性的靜態變數。
Netty中的記憶體單位
Netty中將記憶體池分為五種不同的形態:Arena,ChunkList,Chunk,Page,SubPage.
首先來看Netty最大的記憶體單位PoolArena——連續的記憶體塊。它是由多個PoolChunkList和兩個SubPagePools(一個是tinySubPagePool,一個是smallSubPagePool)組成的。如下圖所示:
1.PoolChunkList是一個雙向的連結串列,PoolChunkList負責管理多個PoolChunk的生命週期。
2.PoolChunk中包含多個Page,Page的大小預設是8192位元組,也可以設定系統變數io.netty.allocator.pageSize來改變頁的大小。自定義頁大小有如下限制:1.必須大於4096位元組,2.必須是2的整次數冪。
3.塊(PoolChunk)的大小是由頁的大小和maxOrder算出來的,計算公式是: chunkSize = 2^{maxOrder} * pageSize。 maxOrder的預設值是11,也可以通過io.netty.allocator.maxOrder系統變數設定,只能是0-14的範圍,所以chunksize的預設大小為:(2^11)*8192=16MB
Page中包含多個SubPage。
PoolChunk內部維護了一個平衡二叉樹,如下圖所示:
PoolSubPage
通常一個頁(page)的大小就達到了10^13(8192位元組),通常一次申請分配記憶體沒有這麼大,可能很小。
於是Netty將頁(page)劃分成更小的片段——SubPage
Netty定義這樣的記憶體單元是為了更好的分配記憶體,接下來看一下一個ByteBuf是如何在記憶體池中申請記憶體的。
Netty如何分配記憶體池中的記憶體?
分配原則:
記憶體池中的記憶體分配是在PoolArea中進行的。
-
申請小於PageSize(預設8192位元組)的記憶體,會在SubPagePools中進行分配,如果申請記憶體小於512位元組,則會在tingSubPagePools中進行分配,如果大於512小於PageSize位元組,則會在smallSubPagePools進行分配。
-
申請大於PageSize的記憶體,則會在PoolChunkList中進行分配。
-
申請大於ChunkSize的記憶體,則不會在記憶體池中申請,而且也不會重用該記憶體。
應用中在記憶體池中申請記憶體的方法:
// 在記憶體池中申請 直接記憶體 ByteBuf directByteBuf = ByteBufAllocator.DEFAULT.directBuffer(1024); // 在記憶體池中申請 堆記憶體 ByteBuf heapByteBuf = ByteBufAllocator.DEFAULT.heapBuffer(1024);
接下來,一層一層的看下來,在Netty中申請記憶體是如何實現的。就拿申請直接記憶體舉例,首先看directBuffer方法。
// directBuffer方法實現 @Override
public ByteBuf directBuffer(int initialCapacity) {
return directBuffer(initialCapacity, DEFAULT_MAX_CAPACITY); }
// 校驗申請大小,返回申請的直接記憶體
@Override
public ByteBuf directBuffer(int initialCapacity, int maxCapacity) {
if (init ialCapacity == 0 && maxCapacity == 0) {
return emptyBuf; }
validate(initialCapacity, maxCapacity);
return newDirectBuffer(initialCapacity, maxCapacity);
}
//PooledByteBufAllocator類中的 newDirectBuffer方法的實現
@Override
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
// Netty避免每個執行緒對記憶體池的競爭,在每個執行緒都提供了PoolThreadCache執行緒內的內池
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);
}
// 通過toLeakAwareBuffer包裝成記憶體洩漏檢測的buffer
return toLeakAwareBuffer(buf);
}
一般情況下,記憶體都是在buf = directArena.allocate(cache, initialCapacity, maxCapacity)這行程式碼進行記憶體分配的,也就是說在記憶體的連續塊PoolArena中進行的記憶體分配。
接下來,我們根據記憶體分配原則來進行記憶體研讀PoolArena中的allocate方法。
1 PooledByteBuf<T> allocate(PoolThreadCache cache, int reqCapacity, int maxCapacity) { 2 PooledByteBuf<T> buf = newByteBuf(maxCapacity); 3 allocate(cache, buf, reqCapacity); 4 return buf; 5 } 6 7 private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) { 8 final int normCapacity = normalizeCapacity(reqCapacity); 9 if (isTinyOrSmall(normCapacity)) { // capacity < pageSize10 int tableIdx;11 PoolSubpage<T>[] table;12 boolean tiny = isTiny(normCapacity);13 if (tiny) { // < 51214 15 // 如果申請記憶體小於512位元組,則會在tingSubPagePools中進行分配16 if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {17 // was able to allocate out of the cache so move on18 return;19 }20 tableIdx = tinyIdx(normCapacity);21 table = tinySubpagePools;22 } else {23 // 如果大於512小於PageSize位元組,則會在smallSubPagePools進行分配24 if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {25 // was able to allocate out of the cache so move on26 return;27 }28 tableIdx = smallIdx(normCapacity);29 table = smallSubpagePools;30 }31 32 final PoolSubpage<T> head = table[tableIdx];33 34 /** 35 * Synchronize on the head. This is needed as {@link PoolChunk#allocateSubpage(int)} and 36 * {@link PoolChunk#free(long)} may modify the doubly linked list as well. 37 */38 synchronized (head) {39 final PoolSubpage<T> s = head.next;40 if (s != head) {41 assert s.doNotDestroy && s.elemSize == normCapacity;42 long handle = s.allocate();43 assert handle >= 0;44 s.chunk.initBufWithSubpage(buf, handle, reqCapacity);45 incTinySmallAllocation(tiny);46 return;47 }48 }49 synchronized (this) {50 allocateNormal(buf, reqCapacity, normCapacity);51 }52 53 incTinySmallAllocation(tiny);54 return;55 }56 if (normCapacity <= chunkSize) {57 if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {58 // was able to allocate out of the cache so move on59 return;60 }61 synchronized (this) {62 allocateNormal(buf, reqCapacity, normCapacity);63 ++allocationsNormal;64 }65 } else {66 // Huge allocations are never served via the cache so just call allocateHuge67 allocateHuge(buf, reqCapacity);68 }69 }
如何使用記憶體池?
底層IO處理執行緒的緩衝區使用堆外直接緩衝區,減少一次IO複製。業務訊息的編解碼使用堆緩衝區,分配效率更高,而且不涉及到核心緩衝區的複製問題。
Netty預設不使用記憶體池,需要在建立服務端或者客戶端的時候進行配置。
//Boss執行緒池記憶體池配置. .option(ChannelOption.ALLOCATOR,PooledByteBufAllocator.DEFAULT)
//Work執行緒池記憶體池配置.
.childOption(ChannelOption.ALLOCATOR,PooledByteBufAllocator.DEFAULT);
本人的想法是:
1.I/O處理執行緒使記憶體池中的直接記憶體,開啟以上配置
2.在handler處理業務的時候,使用記憶體池中的堆記憶體
還有一點值得注意的是:在使用完記憶體池中的ByteBuf,一定要記得釋放,即呼叫release():
// 在記憶體池中申請 直接記憶體 ByteBuf directByteBuf = ByteBufAllocator.DEFAULT.directBuffer(1024); // 歸還到記憶體池 directByteBuf.release();
如果handler繼承了SimpleChannelInboundHandler,那麼它將會自動釋放Bytefuf.詳情可見:
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { boolean release = true;
try {
if (acceptInboundMessage(msg)) {
@SuppressWarnings("unchecked")
I imsg = (I) msg;
channelRead0(ctx, imsg);
} else {
release = false;
ctx.fireChannelRead(msg);
}
} finally { // autoRelease預設為true
if (autoRelease && release) { // 釋放Bytebuf,歸還到記憶體池
ReferenceCountUtil.release(msg);
} } }
零拷貝:
該部分是重點介紹的部分,首先將它與傳統的I/O read和write操作作對比,看看有什麼不同,首先需要理解一下使用者態和記憶體態的概念
使用者態(User Mode)和核心態(Kernel Mode),也可以叫使用者空間和核心
使用者態:受限的訪問記憶體,並且不允許訪問硬體裝置。
核心態:本質上是一個軟體,可以控制計算機的硬體資源(如網絡卡,硬碟),可以訪問記憶體所有資料。
使用者程式都是執行在使用者態中的,比如JVM,就是使用者程式,所以它執行在使用者態中。
使用者態是不能直接訪問硬體裝置的,如果需要一次I/O操作,那就必須利用系統呼叫機制切換到核心態(使用者態與核心態之間的轉換稱為上下文切換),進行硬碟讀寫。
比如說一次傳統網路I/O:
第一步,從使用者態切換到核心態,將使用者緩衝區的資料拷貝到核心緩衝區,執行send操作。
第二步,資料傳送由底層的作業系統進行,此時從核心態切換到使用者態,將核心快取區的資料拷貝到網絡卡的緩衝區
總結:也就是一次普通的網路I/O,至少經過兩次上下文切換,和兩次記憶體拷貝。
什麼是零拷貝?
當需要傳輸的資料遠大於核心緩衝區的大小時,核心緩衝區就成為I/O的效能瓶頸。零拷貝就是杜絕了核心緩衝區與使用者緩衝區的的資料拷貝。
所以零拷貝適合大資料量的傳輸。
拿傳統的網路I/O做對比,零拷貝I/O是怎樣的一個過程:
使用者程式執行transferTo(),將使用者緩衝區待發送的資料拷貝到網絡卡緩衝區。
很簡單,一步完成,中間少了使用者態到記憶體態的拷貝。
Netty中零拷貝如何實現
Netty的中零拷貝與上述零拷貝是不一樣的,它並不是系統層面上的零拷貝,只是相對於ByteBuf而言的。
Netty中的零拷貝:
1.CompositeByteBuf,將多個ByteBuf合併為一個邏輯上的ByteBuf,避免了各個ByteBuf之間的拷貝。
使用方式:
CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer(); compositeByteBuf.addComponents(true, ByteBuf1, ByteBuf1);
注意: addComponents第一個引數必須為true,那麼writeIndex才不為0,才能從compositeByteBuf中讀到資料。
2.wrapedBuffer()方法,將byte[]陣列包裝成ByteBuf物件。
byte[] bytes = data.getBytes();ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);
Unpooled.wrappedBuffer(bytes)就是進行了byte[]陣列的包裝工作,過程中不存在記憶體拷貝。
即包裝出來的ByteBuf和byte[]陣列指向了同一個儲存空間。因為值引用,所以bytes修改也會影響 byteBuf 的值。
3.ByteBuf的分割,slice()方法。將一個ByteBuf物件切分成多個ByteBuf物件。
ByteBuf directByteBuf = ByteBufAllocator.DEFAULT.directBuffer(1024);ByteBuf header = directByteBuf.slice(0,50);ByteBuf body = directByteBuf.slice(51,1024);
header和body兩個ByteBuf物件實際上還是指向directByteBuf的儲存空間。
總結:
本文很長很長,博主陸陸續續寫了有一個月的時間。但是隻是窺探Netty記憶體池中的冰山一角,更多是要在實際專案中進行驗證才能起到效果。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/28285180/viewspace-2200179/,如需轉載,請註明出處,否則將追究法律責任。