天天看点

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对象

继续阅读