Netty中實作引用計數的接口是
ReferenceCounted
,一個實作了
ReferenceCounted
接口的類就具備了引用計數的能力。
當一個引用計數的對象被初始化時, 引用計數器被初始化為1。
ReferenceCounted
接口中兩個主要的方法:
retain()方法對引用計數加1,release()方法對引用計數減1。如果引用計數被減到0,則這個對象就将被顯式的回收,此時再來通路該對象則會抛出
IllegalReferenceCountException
異常。
當一個引用計數對象被初始化後,引用計數器的值為1,如圖image1所示:
當調用retain()方法,引用計數器的值加1,如圖image2所示:
當調用retain(2)方法,引用計數器的值加2,如圖image3所示:
當調用release(2)方法,引用計數器的值減2,如圖image4所示:
當調用release()方法,引用計數器的值減1,如圖image5所示:
當調用release()方法,引用計數器的值減1,當引用計數器的值變為0時,該引用計數對象将被回收,如圖image6所示:
使用不當常常會引起
IllegalReferenceCountException
異常,該異常的定義如下:
public IllegalReferenceCountException(int refCnt, int increment) {
this("refCnt: " + refCnt + ", " + (increment > ? "increment: " + increment : "decrement: " + -increment));
}
相信這段異常,接觸過netty的一定不會陌生。
其中
refCnt
表示的是目前引用計數的值,
increment
表示要增加的計數值。
PS:當
increment
大于0,則表示是在增加引用計數時出現錯誤,否則是在減少引用計數時出現的錯誤。
該異常在retain()和release()方法中都有可能會抛出:
- 在retain方法中
private ReferenceCounted retain0(int increment) {
for (;;) {
int refCnt = this.refCnt;
final int nextCnt = refCnt + increment;
// 當nextCnt<=increment時,表面refCnt==0,說明該對象已經被回收了
// 若此時再來增加引用計數的值,則抛出非法引用的異常
if (nextCnt <= increment) {
throw new IllegalReferenceCountException(refCnt, increment);
}
if (refCntUpdater.compareAndSet(this, refCnt, nextCnt)) {
break;
}
}
return this;
}
- 在release方法中
private boolean release0(int decrement) {
for (;;) {
int refCnt = this.refCnt;
// 由于decrement最小為1,要滿足refCnt<1成立,則refCnt==0,則說明該對象已經被回收了
// 若此時再來減少引用計數的值,則抛出非法引用的異常
if (refCnt < decrement) {
throw new IllegalReferenceCountException(refCnt, -decrement);
}
if (refCntUpdater.compareAndSet(this, refCnt, refCnt - decrement)) {
if (refCnt == decrement) {
deallocate();
return true;
}
return false;
}
}
}
讓我來構造一個出現該異常的例子,舉例說明一下:
調用write()方法引起的引用計數異常
一個簡單的服務端demo如下,該服務端實作的功能很簡單,往前端輸出一段json字元串:
{"echo":"hello,netty"}
public class QueenServerHandler extends ChannelInboundHandlerAdapter {
private static final String CONTENT_TYPE_JSON = "application/json;charset=UTF-8";
private ChannelHandlerContext ctx;
private static final ByteBuf BUF = Unpooled.copiedBuffer("{\"echo\":\"hello,netty\"}",CharsetUtil.UTF_8);
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
this.ctx = ctx;
response(BUF);
}
private void response(ByteBuf buf){
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buf);
response.headers().add(HttpHeaderNames.CONTENT_TYPE, CONTENT_TYPE_JSON);
writeResponse(response);
}
private void writeResponse(Object response){
ChannelFuture future = ctx.channel().writeAndFlush(response);
future.addListener(ChannelFutureListener.CLOSE);
}
}
該代碼之後,在浏覽器上發起請求,第一次系統将正常傳回
{"echo":"hello,netty"}
的内容,從第二次開始請求時,服務端将抛出下面的異常,如圖image8所示:
從異常抛出的資訊:
decrement: 1
可以知道,應該是在調用release()方法時抛出的錯。
從異常的堆棧中可以發現,是在調用write()方法時出現的錯,在MessageToMessageEncoder.write()的方法中,發現了調用release的代碼,如圖image9所示:
是以到這裡,異常出現的原因就很清楚了:
該ByteBuf對象是一個類變量:
private static final ByteBuf BUF;
,第一次調用release時已經成功将BUF對象回收了,之後再次調用release時,就會抛出
IllegalReferenceCountException
異常了。
知道原因之後,解決的方法就很簡單了,有兩種方法:
- 1.将BUF對象改為局部變量,每次write的時候都建立一個新的BUF對象,之後每次再調用release就不會有問題了。
- 2.每次使用時擷取BUF的一個拷貝,用BUF.copy()來傳回一個新的ByteBuf對象