Netty引用计数
Netty中实现引用计数的接口是ReferenceCounted
,一个实现了ReferenceCounted
接口的类就具备了引用计数的能力。
当一个引用计数的对象被初始化时, 引用计数器被初始化为1。
ReferenceCounted
接口中两个主要的方法:
retain()方法对引用计数加1,release()方法对引用计数减1。如果引用计数被减到0,则这个对象就将被显式的回收,此时再来访问该对象则会抛出IllegalReferenceCountException
异常。
当一个引用计数对象被初始化后,引用计数器的值为1,如图image1所示:
image1.png当调用retain()方法,引用计数器的值加1,如图image2所示:
image2.png当调用retain(2)方法,引用计数器的值加2,如图image3所示:
image3.png当调用release(2)方法,引用计数器的值减2,如图image4所示:
image4.png当调用release()方法,引用计数器的值减1,如图image5所示:
image5.png当调用release()方法,引用计数器的值减1,当引用计数器的值变为0时,该引用计数对象将被回收,如图image6所示:
image6.png使用不当常常会引起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所示:
从异常抛出的信息: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对象
欢迎关注微信公众号我是逅弈,如果文章对您有帮助,欢迎您点赞加关注,并欢迎您关注我的公众号: