本节摘要:ByteBuf 用读写双指针取代 ByteBuffer 的单指针加 flip,用引用计数实现跨工位的所有权转移,并提供切片、组合、扩容等原料操作。本节覆盖 API 实操、四类容器的选择,以及每条消息"从分配到释放"的生命周期纪律。
JDK ByteBuffer 的读写共用一根指针:写完要读,必须先 flip() 把 limit 设到写入位置、position 归零;读完想再写,又要 clear() 或 compact()。忘一次 flip,读出来全是空;在 Netty 这种"层层加工"的场景里,每次工位交接都要翻转指针,出错概率随工位数线性上升。
ByteBuf 的解法是把读写拆成两根指针:readerIndex 与 writerIndex,中间是可读区域,各自独立前进,互不需要翻转。再加上容量自动扩容、支持池化与堆外,它就是为流水线而生的容器。

ByteBuf buf = Unpooled.buffer(16); // 建一只箱子(先非池化练手) // 写:writerIndex 前进 buf.writeInt(1024); buf.writeBytes("hello".getBytes(StandardCharsets.UTF_8)); // 读:readerIndex 前进 int v = buf.readInt(); // 1024 ByteBuf dup = buf.readBytes(5); // "hello",消费型读取 System.out.println(buf.readableBytes()); // 0 // 常用查询 buf.isReadable(); buf.readerIndex(); buf.writerIndex(); buf.capacity(); // 回收已读空间(涉及搬移,写密集场景别频繁调) buf.discardReadBytes(); // 切片与复制:slice 共享底层数组,copy 真拷贝 ByteBuf header = buf.slice(0, 4); // 零拷贝视图(见 7.1 节) ByteBuf copy = buf.copy(0, 4); // 独立副本
四类常见容器的选择:Unpooled.buffer 堆内(测试、小数据);Unpooled.directBuffer 堆外(避免一次内核到堆的拷贝,网络 IO 推荐);PooledByteBufAllocator.DEFAULT 池化分配(服务端默认,4.2 主角);EmptyByteBuf 与只读包装等特殊形态按需。
ByteBuf 实现 ReferenceCounted 接口:创建时计数为 1,retain() 加一,release() 减一,减到零容器销毁(池化的归还仓库,非池化的释放内存)。这条账本的意义在于解决一个问题:一条消息在流水线上经过多个工位,谁负责扔箱子?
Netty 的约定:谁最后消费,谁释放;往下传,就必须 retain 或使用自动传播的 fire 方法。第三章的 fireChannelRead 会把所有权移交给下游;SimpleChannelInboundHandler 作为典型终点自动 release;而 ChannelInboundHandlerAdapter 透传时调 fireChannelRead(msg) 即可,不需要自己释放。
// 终点工位:自动释放(Simple 系) class BizHandler extends SimpleChannelInboundHandler<ByteBuf> { protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) { process(msg); // 用完即由基类释放 } } // 分叉工位:既要自己用又要传下去,先复制或 retain class ForkHandler extends ChannelInboundHandlerAdapter { public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf b = (ByteBuf) msg; if (b.refCnt() > 0) { ByteBuf retained = b.retainedSlice(); // 借出一份视图 asyncStore(retained); // 异步存档,用完自行 release } ctx.fireChannelRead(msg); // 原件继续向下 } }
漏 release 的症状:堆外内存缓慢上涨,最终 OutOfDirectMemoryError;池化则触发泄漏告警(4.3 详讲)。多 release 的症状更直接:IllegalReferenceCountException,日志里能看到计数归零后又被操作的位置。
⚠️ 常见坑:把 ByteBuf 存进集合或异步任务里,流水线继续往下传,下游 release 归零,异步任务再访问就是越界读取。跨出流水线生命周期的持有,必须
retain配对release。
| 场景 | 正确动作 |
|---|---|
| 收到消息只审计,继续传 | fireChannelRead(msg),不释放 |
| 消息到此消费完毕 | 用 SimpleChannelInboundHandler,自动释放 |
| 部分消费后传剩余 | discardReadBytes 后 fire,或传 slice 并 retain |
| 转交异步线程处理 | retainedDuplicate 或 retain,异步侧用完 release |
| 发往远端(writeAndFlush) | Netty 出站写完自动释放,无需手工 |
💡 关键直觉:把 ByteBuf 当"带押金的周转箱":领箱押金记一,转让要过账(retain),用完退押金(release);账本归零箱子回收。流水线上的默认路径 Netty 都替你记账了,只有"拿出去用"时才需要自己动账本。