Netty之引用计数

Netty中实现引用计数的接口是ReferenceCounted,一个实现了ReferenceCounted接口的类就具备了引用计数的能力。
当一个引用计数的对象被初始化时, 引用计数器被初始化为1。
ReferenceCounted接口中两个主要的方法:
retain()方法对引用计数加1,release()方法对引用计数减1。如果引用计数被减到0,则这个对象就将被显式的回收,此时再来访问该对象则会抛出IllegalReferenceCountException异常。

当一个引用计数对象被初始化后,引用计数器的值为1,如图image1所示:

Netty之引用计数_第1张图片

当调用retain()方法,引用计数器的值加1,如图image2所示:

Netty之引用计数_第2张图片

当调用retain(2)方法,引用计数器的值加2,如图image3所示:

Netty之引用计数_第3张图片

当调用release(2)方法,引用计数器的值减2,如图image4所示:

Netty之引用计数_第4张图片

当调用release()方法,引用计数器的值减1,如图image5所示:

Netty之引用计数_第5张图片

当调用release()方法,引用计数器的值减1,当引用计数器的值变为0时,该引用计数对象将被回收,如图image6所示:

Netty之引用计数_第6张图片

使用不当常常会引起IllegalReferenceCountException异常,该异常的定义如下:

public IllegalReferenceCountException(int refCnt, int increment) {
    this("refCnt: " + refCnt + ", " + (increment > 0? "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之引用计数_第7张图片

从异常抛出的信息:decrement: 1可以知道,应该是在调用release()方法时抛出的错。
从异常的堆栈中可以发现,是在调用write()方法时出现的错,在MessageToMessageEncoder.write()的方法中,发现了调用release的代码,如图image9所示:

Netty之引用计数_第8张图片

所以到这里,异常出现的原因就很清楚了:
该ByteBuf对象是一个类变量:private static final ByteBuf BUF;,第一次调用release时已经成功将BUF对象回收了,之后再次调用release时,就会抛出IllegalReferenceCountException异常了。
知道原因之后,解决的方法就很简单了,有两种方法:

  • 1.将BUF对象改为局部变量,每次write的时候都创建一个新的BUF对象,之后每次再调用release就不会有问题了。
  • 2.每次使用时获取BUF的一个拷贝,用BUF.copy()来返回一个新的ByteBuf对象

你可能感兴趣的:(Netty之引用计数)