当某个 ChannelInboundHandler 的实现重写 channelRead()方法时,它要负责显式地释放与池化的 ByteBuf 实例相关的内存。
Netty 为此提供了一个实用方法 ReferenceCountUtil.release() Netty 将使用 WARN 级别的日志消息记录未释放的资源,使得可以非常简单地在代码 中发现违规的实例。但是以这种方式管理资源可能很繁琐。一个更加简单的方式是使用 SimpleChannelInboundHandler,SimpleChannelInboundHandler 会自动释放资源。
1、对于入站请求,Netty 的 EventLoop 在处理 Channel 的读操作时进行分配 ByteBuf,对 于这类 ByteBuf,需要我们自行进行释放。
有三种方式

  • 使用 SimpleChannelInboundHandler
  • 在重写 channelRead()方法使用 ReferenceCountUtil.release()
  • 使用 ctx.fireChannelRead 继续向后传递;

2、对于出站请求,不管 ByteBuf 是否由我们的业务创建的,当调用了 write 或者 writeAndFlush 方法后,Netty 会自动替我们释放,不需要我们业务代码自行释放。


Netty资源释放原理

由于 Netty 中有堆外内存的 ByteBuf 实现,堆外内存最好是手动来释放,而不是等 GC 垃圾回收。

  • UnpooledHeapByteBuf 使用的是 JVM 内存,只需等 GC 回收内存即可
  • UnpooledDirectByteBuf 使用的就是直接内存了,需要特殊的方法来回收内存
  • PooledByteBuf 和它的子类使用了池化机制,需要更复杂的规则来回收内存

Netty 这里采用了引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted 接口

  • 每个 ByteBuf 对象的初始计数为 1
  • 调用 release 方法计数减 1,如果计数为 0,ByteBuf 内存被回收
  • 调用 retain 方法计数加 1,表示调用者没用完之前,其它 handler 即使调用了 release 也不会造成回收
  • 当计数为 0 时,底层内存会被回收,这时即使 ByteBuf 对象还在,其各个方法均无法正常使用

谁来负责 release 呢?
不是我们想象的(一般情况下)

  1. ByteBuf buf = ...
  2. try {
  3. ...
  4. } finally {
  5. buf.release();
  6. }

请思考,因为 pipeline 的存在,一般需要将 ByteBuf 传递给下一个 ChannelHandler,如果在 finally 中 release 了,就失去了传递性(当然,如果在这个 ChannelHandler 内这个 ByteBuf 已完成了它的使命,那么便无须再传递)

基本规则是,谁是最后使用者,谁负责 release。

  • 起点,对于 NIO 实现来讲,在 io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe#read 方法中首次创建 ByteBuf 放入 pipeline(line 163 pipeline.fireChannelRead(byteBuf))

    1.入站 ByteBuf 处理原则

    • 对原始 ByteBuf 不做处理,调用 ctx.fireChannelRead(msg) 向后传递,这时无须 release
    • 将原始 ByteBuf 转换为其它类型的 Java 对象,这时 ByteBuf 就没用了,必须 release
    • 如果不调用 ctx.fireChannelRead(msg) 向后传递,那么也必须 release
    • 注意各种异常,如果 ByteBuf 没有成功传递到下一个 ChannelHandler,必须 release
    • 假设消息一直向后传,那么 TailContext 会负责释放未处理消息(原始的 ByteBuf)

      2.出站 ByteBuf 处理原则

    • 出站消息最终都会转为 ByteBuf 输出,一直向前传,由 HeadContext flush 后 release

      3.异常处理原则

    • 有时候不清楚 ByteBuf 被引用了多少次,但又必须彻底释放,可以循环调用 release 直到返回 true

TailContext 释放未处理消息逻辑

  1. // io.netty.channel.DefaultChannelPipeline#onUnhandledInboundMessage(java.lang.Object)
  2. protected void onUnhandledInboundMessage(Object msg) {
  3. try {
  4. logger.debug(
  5. "Discarded inbound message {} that reached at the tail of the pipeline. " +
  6. "Please check your pipeline configuration.", msg);
  7. } finally {
  8. ReferenceCountUtil.release(msg);
  9. }
  10. }

具体代码

  1. // io.netty.util.ReferenceCountUtil#release(java.lang.Object)
  2. public static boolean release(Object msg) {
  3. if (msg instanceof ReferenceCounted) {
  4. return ((ReferenceCounted) msg).release();
  5. }
  6. return false;
  7. }