第 8 / 20 篇

第 7 章 ByteBuf 与内存所有权

7.1 ByteBuf 里有两个重要下标

ByteBuf 是 Netty 的字节容器。可以先把它理解为带读写位置的字节数组:[4]

0             readerIndex          writerIndex        capacity
| 已读区域       | 可读区域             | 可写区域            |

readableBytes() 表示当前可读字节数。readInt() 读取 4 字节并移动读下标;getInt(index) 读取指定位置而不移动读下标。writeInt 写入并推进写下标。

读取前必须确认有足够字节。如果自己实现 ByteToMessageDecoder,当数据不足时应直接返回,并且不要提前消耗无法恢复的头部。必要时使用绝对读取检查长度,完整后才移动下标。

7.2 为什么需要 release

部分 ByteBuf 使用直接内存或内存池,生命周期不能只依赖 Java 垃圾回收。引用计数用于表示还有多少使用者持有它。retain() 增加计数,release() 减少计数;到 0 后对象不可再用。[5]

最实用的原则是:接收并消费消息的一方负责释放;把消息转交给下一环节时,通常同时转交释放责任。 具体仍要服从所用处理器和 API 的契约。

在普通 ChannelInboundHandlerAdapter 中消费 ByteBuf,可以这样写:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    try {
        ByteBuf buf = (ByteBuf) msg;
        System.out.println(buf.readableBytes());
    } finally {
        ReferenceCountUtil.release(msg);
    }
}

这是额外示意代码,需要导入对应类型。如果调用 ctx.fireChannelRead(msg) 转发原对象,通常不应在同一路径再释放一次。若使用默认自动释放的 SimpleChannelInboundHandler<ByteBuf>,则不要在回调结束时额外手动释放同一份所有权。

7.3 slice、duplicate 和 copy

slice() 与 duplicate() 通常共享底层内容,也不自动增加引用计数。源缓冲区释放后,派生视图不能凭空继续使用。需要独立延长生命周期时,可按契约使用 retainedSlice()、retainedDuplicate(),并在使用结束时释放。

copy() 复制内容,得到独立的缓冲区,代价是分配与复制。初学时先追求清楚的所有权,再考虑减少复制。

7.4 测试中的 ByteBuf 也要释放

EmbeddedChannel.readOutbound() 取出的 ByteBuf 已经交给测试代码。测试读取后应在 finally 中 release()。测试结束调用 finishAndReleaseAll(),清理仍留在通道里的消息;已经被取走的对象仍由取走者负责。

遇到 IllegalReferenceCountException,检查是否重复释放或释放后使用。遇到泄漏告警,检查异常分支、消息丢弃分支和跨线程转交分支。开发排查时可打开更严格的泄漏检测,但它会增加开销。

小练习: 在 SimpleChannelInboundHandler<ByteBuf> 中,为什么不能直接把收到的 buf 存到列表里,等一分钟后再读?