不論我們在前面學習NIO的ByteBuffer,還是現在Netty當中的ByteBuf,其都有使用直接內存的方式。
在Netty當中,我們使用完直接內存,需要去手動進行釋放,而不應該等待GC去進行回收,以減少發生內存溢出的風險。
一、ByteBuf的種類
關於其種類,有很多種,我們根據前面提到的池化機制,將其主要分爲兩大類,每一類噹噹中又分爲堆內存和直接內存:
- UnpooledHeapByteBuf:非池化堆內存ByteBuf,受JVM內存管理,可以等待GC回收。
- UnpooledDirectByteBuf:非池化直接內存ByteBuf,不收JVM管理,雖然可以受GC回收,但不是及時的,可能會發生內存溢出,需要手動進行回收。
- PooledByteBuf:池化ByteBuf,這種有更復雜的回收範式,後面通過源碼分析,具體查看其實現細節。
- PooledHeapByteBuf:池化堆內存ByteBuf
- PooledDirectByteBuf:池化直接內存ByteBuf
二、直接內存回收原理
在前面的文章中,我們簡單聊到過ByteBuf的結構:
public abstract class ByteBuf implements ReferenceCounted
如上所示,其實現了ReferenceCounted的接口,接口翻譯過來叫做“引用計數”。
相信學過jvm GC的同學應該有所瞭解“引用計數法”,當一個對象有引用時,我們就對計數器加1,反之就減1,但是引用計數法無法處理環形垃圾,所以後面提出了“根可達算法”,簡單提一下,需要了解細節的朋友可以看我的專題【JVM】。
此處的引用計數,用於ByteBuf的直接內存回收,我們看下其主要的方法:
public interface ReferenceCounted {
/**
* 返回當前對象的引用計數
*/
int refCnt();
/**
* 將引用計數增加1
*/
ReferenceCounted retain();
/**
* 按指定的increment增加引用計數
*/
ReferenceCounted retain(int increment);
/**
* 將引用計數減少1,並在引用計數達到0解除分配此對象
*/
boolean release();
/**
* 將引用計數減少指定的decrement ,如果引用計數達到0則取消分配此對象。
*/
boolean release(int decrement);
}
所有的ByteBuf都會實現這個接口,當一個新的ReferenceCounted被實例化時,它以1的引用計數開始。 retain()增加引用計數,而release()減少引用計數。 如果引用計數減少到0 ,對象將被釋放,並且訪問釋放的對象通常會導致訪問衝突。
通過下面的代碼簡單試用一下:
public static void main(String[] args) {
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer();
//打印當前的引用計數
System.out.println("初始化後的引用計數" + byteBuf.refCnt());
//釋放引用計數
byteBuf.release();
//打印當前的引用計數
System.out.println("釋放後的引用計數" + byteBuf.refCnt());
//調用byteBuf
try {
byteBuf.writeInt(888);
} catch (Exception e) {
System.out.println("釋放後調用異常:" + e);
}
//增加引用計數
try {
byteBuf.retain();
} catch (Exception e) {
System.out.println("釋放後增加引用計數異常:" + e);
}
// 重新分配
byteBuf = ByteBufAllocator.DEFAULT.buffer();
//調用byteBuf
byteBuf.writeInt(888);
System.out.println("重新分配後的引用計數" + byteBuf.refCnt());
}
結果:
初始化後的引用計數1
釋放後的引用計數0
釋放後調用異常:io.netty.util.IllegalReferenceCountException: refCnt: 0
釋放後增加引用計數異常:io.netty.util.IllegalReferenceCountException: refCnt: 0, increment: 1
重新分配後的引用計數1
當引用計數變爲0後,整個內存就釋放了,再次使用會拋出異常,重新嘗試增加引用計數也會跑出異常,只能進行重新分配。
三、內存釋放使用方式
3.1 手動釋放
前面簡單瞭解了關於內存釋放的內容,那麼我們應該如何使用呢?是不是可以向我們習慣的java代碼一樣,在finally當中調用呢?
try {
} finally {
byteBuf.release();
}
直接給出結論,是不行的。
前面我們介紹時候就說過,會有機率造成內存溢出的,即使不會發生也會造成內存的浪費。
前面的文章當中,我們學習了Pipeline和Handler。通常我們會將一個byteBuf傳遞給另一個channelHandler去處理,是存在一個傳遞性的。這裏面存在兩種情況:
- 假設一共有5個channelHandler,在第二個當中,將byteBuf轉換成了java對象,然後將對象傳遞給第三個channelHandler,此時byteBuf就沒有用了,所以此時就應該釋放。
- 一直以byteBuf傳遞,直到最後一個channelHandler才進行釋放。
總結一句話:最後誰用完了,誰就負責釋放。
建議:如果確定這個buf在最後時刻用完了,而又無法確定當前有多少個引用計數,使用如下兩種方式釋放:
- 循環調用release(),知道返回true。
- 通過refCnt()獲取當前的引用計數,然後調用release(int refCnt)釋放。
3.2 tail和head自動釋放
還記得前面將Pipeline和Handler時,提到了關於head和tail的概念,除了我們自己添加的Handler以外,會默認有一個頭和尾的處理器。
在這兩個處理器當中,也會有自動回收內存的保底能力,但是前提是要求我們將byteBuf傳遞到head或tail當中纔行,對於中途就轉換類型的,仍然需要我們自己去釋放資源。
前面我們還學習過入站處理器和出棧處理器,其中入站處理器傳遞內容需要使用channelRead()方法,而在出站處理器傳遞參數需要使用write方法,這將作爲我們跟蹤代碼的標記。
下面我們簡單跟蹤下源碼,看看是如何實現的內存釋放。
我們跟蹤pipeline的addLast方法,跟蹤到了AbstractChannelHandlerContext這個抽象類,其有兩個實現類:
剛好對應我們的head和tail處理器。
3.2.1 TailContext
首先看tail處理器,實現了ChannelInboundHandler,即入站處理器,進行入站首尾工作。
final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler
找到channelRead方法:
public void channelRead(ChannelHandlerContext ctx, Object msg) {
DefaultChannelPipeline.this.onUnhandledInboundMessage(ctx, msg);
}
繼續跟蹤onUnhandledInboundMessage
protected void onUnhandledInboundMessage(Object msg) {
try {
logger.debug("Discarded inbound message {} that reached at the tail of the pipeline. Please check your pipeline configuration.", msg);
} finally {
ReferenceCountUtil.release(msg);
}
}
發現其中的引用計數工具類,調用了release方法:
ReferenceCountUtil.release(msg);
判斷msg是否是實現了ReferenceCounted ?是就進行是否,否則返回false。
public static boolean release(Object msg) {
return msg instanceof ReferenceCounted ? ((ReferenceCounted)msg).release() : false;
}
3.2.1 HeadContext
查看HeadContext,實現了ChannelOutboundHandler,即出站處理器,進行出站首尾工作。
final class HeadContext extends AbstractChannelHandlerContext implements ChannelOutboundHandler, ChannelInboundHandler
找到其write方法:
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
this.unsafe.write(msg, promise);
}
繼續跟蹤write:
public final void write(Object msg, ChannelPromise promise) {
this.assertEventLoop();
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null) {
this.safeSetFailure(promise, this.newClosedChannelException(AbstractChannel.this.initialCloseCause));
ReferenceCountUtil.release(msg);
} else {
int size;
try {
msg = AbstractChannel.this.filterOutboundMessage(msg);
size = AbstractChannel.this.pipeline.estimatorHandle().size(msg);
if (size < 0) {
size = 0;
}
} catch (Throwable var6) {
this.safeSetFailure(promise, var6);
ReferenceCountUtil.release(msg);
return;
}
outboundBuffer.addMessage(msg, size, promise);
}
}
在上面的代碼中,仍然發現了
ReferenceCountUtil.release(msg)
其他代碼此文暫時不做講解了。
無論是head,還是tail,都需要將buf傳遞過來,才能進行釋放。
本文暫時介紹這些,後面繼續,有幫助的話點個贊吧。