netty系列之:JVM中的Reference count原來netty中也有
簡介
為什麼世界上有這麼多JAVA的程式設計師呢?其中一個很重要的原因就是JAVA相對於C++而言,不需要考慮對象的釋放,一切都是由垃圾回收器來完成的。在崇尚簡單的現代編程世界中,會C++的高手越來越少,會JAVA的程式設計師越來越多。
JVM的垃圾回收器中一個很重要的概念就是Reference count,也就是對象的引用計數,用來控制對象是否還被引用,是否可以被垃圾回收。
netty也是運行在JVM中的,所以JVM中的對象引用計數也適用於netty中的對象。這裡我們說的對象引用指的是netty中特定的某些對象,通過對象的引用計數來判斷這些對象是否還被使用,如果不再被使用的話就可以把它們(或它們的共享資源)返回到對象池(或對象分配器)。
這就叫做netty的對象引用計數技術,其中一個最關鍵的對象就是ByteBuf。
ByteBuf和ReferenceCounted
netty中的對象引用計數是從4.X版本開始的,ByteBuf是其中最終要的一個應用,它利用引用計數來提高分配和釋放性能.
先來看一下ByteBuf的定義:
public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf>
可以看到ByteBuf是一個抽象類,它實現了ReferenceCounted的介面。
ReferenceCounted就是netty中對象引用的基礎,它定義了下面幾個非常重要的方法,如下所示:
int refCnt();
ReferenceCounted retain();
ReferenceCounted retain(int increment);
boolean release();
boolean release(int decrement);
其中refCnt返回的是當前引用個數,retain用來增加引用,而release用來釋放引用。
ByteBuf的基本使用
剛分配情況下ByteBuf的引用個數是1:
ByteBuf buf = ctx.alloc().directBuffer();
assert buf.refCnt() == 1;
當調用他的release方法之後,refCnt就變成了0:
boolean destroyed = buf.release();
assert destroyed;
assert buf.refCnt() == 0;
當調用它的retain方法,refCnt就會加一:
ByteBuf buf = ctx.alloc().directBuffer();
assert buf.refCnt() == 1;
buf.retain();
assert buf.refCnt() == 2;
要注意的是,如果ByteBuf的refCnt已經是0了,就表示這個ByteBuf準備被回收了,如果再調用其retain方法,則會拋出IllegalReferenceCountException:refCnt: 0, increment: 1
所以我們必須在ByteBuf還未被回收之前調用retain方法。
既然refCnt=0的情況下,不能調用retain()方法,那麼其他的方法能夠調用嗎?
我們來嘗試調用一下writeByte方法:
try {
buf.writeByte(10);
} catch (IllegalReferenceCountException e) {
log.error(e.getMessage(),e);
}
可以看到,如果refCnt=0的時候,調用它的writeByte方法會拋出IllegalReferenceCountException異常。
這樣看來,只要refCnt=0,說明這個對象已經被回收了,不能夠再使用了。
ByteBuf的回收
既然ByteBuf中保存的有refCnt,那麼誰來負責ByteBuf的回收呢?
netty的原則是誰消費ByteBuf,誰就負責ByteBuf的回收工作。
在實際的工作中,ByteBuf會在channel中進行傳輸,根據誰消費誰負責銷毀的原則,接收ByteBuf的一方,如果消費了ByteBuf,則需要將其回收。
這裡的回收指的是調用ByteBuf的release()方法。
ByteBuf的衍生方法
ByteBuf可以從一個parent buff中衍生出很多子buff。這些子buff並沒有自己的reference count,它們的引用計數是和parent buff共享的,這些提供衍生buff的方法有:ByteBuf.duplicate(), ByteBuf.slice() 和 ByteBuf.order(ByteOrder)。
buf = directBuffer();
ByteBuf derived = buf.duplicate();
assert buf.refCnt() == 1;
assert derived.refCnt() == 1;
因為衍生的byteBuf和parent buff共享引用計數,所以如果要將衍生的byteBuf傳給其他的流程進行處理的話,需要調用retain()方法:
ByteBuf parent = ctx.alloc().directBuffer(512);
parent.writeBytes(...);
try {
while (parent.isReadable(16)) {
ByteBuf derived = parent.readSlice(16);
derived.retain();
process(derived);
}
} finally {
parent.release();
}
...
public void process(ByteBuf buf) {
...
buf.release();
}
ChannelHandler中的引用計數
netty根據是讀消息還是寫消息,可以分為InboundChannelHandler和OutboundChannelHandler,分別用來讀消息和寫消息。
根據誰消費,誰釋放的原則,對Inbound消息來說,讀取完畢之後,需要調用ByteBuf的release方法:
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
try {
...
} finally {
buf.release();
}
}
但是如果你只是將byteBuf重發到channel中供其他的步驟進行處理,則不需要release:
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
...
ctx.fireChannelRead(buf);
}
同樣的在Outbound中,如果只是簡單的重發,則不需要release:
public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) {
System.err.println("Writing: " + message);
ctx.write(message, promise);
}
如果是處理了消息,則需要release:
public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) {
if (message instanceof HttpContent) {
// Transform HttpContent to ByteBuf.
HttpContent content = (HttpContent) message;
try {
ByteBuf transformed = ctx.alloc().buffer();
....
ctx.write(transformed, promise);
} finally {
content.release();
}
} else {
// Pass non-HttpContent through.
ctx.write(message, promise);
}
}
記憶體泄露
因為reference count是netty自身來進行維護的,需要在程式中手動進行release,這樣會帶來一個問題就是記憶體泄露。因為所有的reference都是由程式自己來控制的,而不是由JVM來控制,所以可能因為程式設計師個人的原因導致某些對象reference count無法清零。
為了解決這個問題,默認情況下,netty會選擇1%的buffer allocations樣本來檢測他們是否存在記憶體泄露的情況.
如果發生泄露,則會得到下面的日誌:
LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced leak reporting, specify the JVM option '-Dio.netty.leakDetectionLevel=advanced' or call ResourceLeakDetector.setLevel()
上面提到了一個檢測記憶體泄露的level,netty提供了4種level,分別是:
- DISABLED—禁用泄露檢測
- SIMPLE –默認的檢測方式,佔用1% 的buff。
- ADVANCED – 也是1%的buff進行檢測,不過這個選項會展示更多的泄露資訊。
- PARANOID – 檢測所有的buff。
具體的檢測選項如下:
java -Dio.netty.leakDetection.level=advanced ...
總結
掌握了netty中的引用計數,就掌握了netty的財富密碼!
本文的例子可以參考:learn-netty4
本文已收錄於 //www.flydean.com/43-netty-reference-cound/
最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!