八、ByteBuf的自动释放
在入站处理时,Netty是何时自动创建入站的ByteBuf的呢?
查看Netty源代码,我们可以看到,Netty的Reactor反应器线程会在底层的Java NIO通道读数据时,也就是AbstractNioByteChannel.NioByteUnsafe.read()处,调用ByteBufAllocator方法,创建ByteBuf实例,从操作系统缓冲区把数据读取到Bytebuf实例中,然后调用pipeline.fireChannelRead(byteBuf)方法将读取到的数据包送入到入站处理流水线中。
再看看入站处理时,入站的ByteBuf是如何自动释放的。
方式一:TailHandler自动释放Netty
默认会在ChannelPipline通道流水线的最后添加一个TailHandler末尾处理器,它实现了默认的处理方法,在这些方法中会帮助完成ByteBuf内存释放的工作。
在默认情况下,如果每个InboundHandler入站处理器,把最初的ByteBuf数据包一路往下传,那么TailHandler末尾处理器会自动释放掉入站的ByteBuf实例。
如何让ByteBuf数据包通过流水线一路向后传递呢?
如果自定义的InboundHandler入站处理器继承自ChannelInboundHandlerAdapter适配器,那么可以在InboundHandler的入站处理方法中调用基类的入站处理方法,演示代码如下:
public class DomoHandler extends ChannelInboundHandlerAdapter {
/**
* 出站处理方法
* @param ctx上下文
* @param msg 入站数据包
* @throws Exception可能抛出的异常
*/
@Override
public void channelRead(ChannelHandlerContextctx, Object msg) throws
Exception {
ByteBuf byteBuf = (ByteBuf) msg;
//...省略ByteBuf的业务处理
//自动释放ByteBuf的方法:调用父类的入站方法,将msg向后传递
// super.channelRead(ctx, msg);
}
}
总体来说,如果自定义的InboundHandler入站处理器继承自ChannelInboundHandlerAdapter适配器,那么可以调用以下两种方法来释放ByteBuf内存:
(1)手动释放ByteBuf。具体的方式为调用byteBuf.release()。
(2)调用父类的入站方法将msg向后传递,依赖后面的处理器释放ByteBuf。具体的方式为调用基类的入站处理方法super.channelRead(ctx, msg)。
public class DomoHandler extends ChannelInboundHandlerAdapter {
/**
* 出站处理方法
* @param ctx上下文
* @param msg 入站数据包
* @throws Exception 可能抛出的异常
*/
@Override
public void channelRead(ChannelHandlerContextctx, Object msg) throws
Exception {
ByteBuf byteBuf = (ByteBuf) msg;
//...省略ByteBuf的业务处理
//释放ByteBuf的两种方法
// 方法一:手动释放ByteBuf
byteBuf.release();
//方法二:调用父类的入站方法,将msg向后传递
// super.channelRead(ctx, msg);
}
}
方式二:SimpleChannelInboundHandler自动释放
如果Handler业务处理器需要截断流水线的处理流程,不将ByteBuf数据包送入后边的InboundHandler入站处理器,这时,流水线末端的TailHandler末尾处理器自动释放缓冲区的工作自然就失效了。
在这种场景下,Handler业务处理器有两种选择:
- 手动释放ByteBuf实例。
- 继承SimpleChannelInboundHandler,利用它的自动释放功能。
这里,我们聚焦的是第二种选择:看看SimpleChannelInboundHandler是如何自动释放的。
以入站读数据为例,Handler业务处理器必须继承自SimpleChannelInboundHandler基类。并且,业务处理器的代码必须移动到重写的channelRead0(ctx, msg)方法中。SimpleChannelInboundHandle类的channelRead等入站处理方法,会在调用完实际的channelRead0方法后,帮忙释放ByteBuf实例。
部分源代码如下:
public abstract class SimpleChannelInboundHandler<I> extends
ChannelInboundHandlerAdapter
{
//基类的入站方法
@Override
public void channelRead(ChannelHandlerContextctx, Object msg) throws
Exception {
boolean release = true;
try {
if (acceptInboundMessage(msg)) {
@SuppressWarnings("unchecked")
I imsg = (I) msg;
//调用实际的业务代码,必须由子类继承,并且提供实现
channelRead0(ctx, imsg);
} else {
release = false;
ctx.fireChannelRead(msg);
}
} finally {
if (autoRelease&& release) {
//释放ByteBuf
ReferenceCountUtil.release(msg);
}
}
}
}
在Netty的SimpleChannelInboundHandler类的源代码中,执行完由子类继承的channelRead0()业务处理后,在finally语句代码段中,ByteBuf被释放了一次,如果ByteBuf计数器为零,将被彻底释放掉。
再看看出站处理时,Netty是何时释放出站的ByteBuf的呢?
出站缓冲区的自动释放方式:HeadHandler自动释放。在出站处理流程中,申请分配到的ByteBuf主要是通过HeadHandler完成自动释放的。
出站处理用到的Bytebuf缓冲区,一般是要发送的消息,通常由Handler业务处理器所申请而分配的。例如,在write出站写入通道时,通过调用ctx.writeAndFlush(Bytebufmsg), Bytebuf缓冲区进入出站处理的流水线。在每一个出站Handler业务处理器中的处理完成后,最后数据包(或消息)会来到出站的最后一棒HeadHandler,在数据输出完成后,Bytebuf会被释放一次,如果计数器为零,将被彻底释放掉。
在Netty开发中,必须密切关注Bytebuf缓冲区的释放,如果释放不及时,会造成Netty的内存泄露(Memory Leak),最终导致内存耗尽。
网友评论