天天看點

Netty之引用計數

Netty中實作引用計數的接口是

ReferenceCounted

,一個實作了

ReferenceCounted

接口的類就具備了引用計數的能力。

當一個引用計數的對象被初始化時, 引用計數器被初始化為1。

ReferenceCounted

接口中兩個主要的方法:

retain()方法對引用計數加1,release()方法對引用計數減1。如果引用計數被減到0,則這個對象就将被顯式的回收,此時再來通路該對象則會抛出

IllegalReferenceCountException

異常。

當一個引用計數對象被初始化後,引用計數器的值為1,如圖image1所示:

Netty之引用計數

當調用retain()方法,引用計數器的值加1,如圖image2所示:

Netty之引用計數

當調用retain(2)方法,引用計數器的值加2,如圖image3所示:

Netty之引用計數

當調用release(2)方法,引用計數器的值減2,如圖image4所示:

Netty之引用計數

當調用release()方法,引用計數器的值減1,如圖image5所示:

Netty之引用計數

當調用release()方法,引用計數器的值減1,當引用計數器的值變為0時,該引用計數對象将被回收,如圖image6所示:

Netty之引用計數

使用不當常常會引起

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所示:

Netty之引用計數

從異常抛出的資訊:

decrement: 1

可以知道,應該是在調用release()方法時抛出的錯。

從異常的堆棧中可以發現,是在調用write()方法時出現的錯,在MessageToMessageEncoder.write()的方法中,發現了調用release的代碼,如圖image9所示:

Netty之引用計數

是以到這裡,異常出現的原因就很清楚了:

該ByteBuf對象是一個類變量:

private static final ByteBuf BUF;

,第一次調用release時已經成功将BUF對象回收了,之後再次調用release時,就會抛出

IllegalReferenceCountException

異常了。

知道原因之後,解決的方法就很簡單了,有兩種方法:

  • 1.将BUF對象改為局部變量,每次write的時候都建立一個新的BUF對象,之後每次再調用release就不會有問題了。
  • 2.每次使用時擷取BUF的一個拷貝,用BUF.copy()來傳回一個新的ByteBuf對象

繼續閱讀