4.1 ByteBuf:会记账的原料箱


4.1 ByteBuf:会记账的原料箱

本节摘要:ByteBuf 用读写双指针取代 ByteBuffer 的单指针加 flip,用引用计数实现跨工位的所有权转移,并提供切片、组合、扩容等原料操作。本节覆盖 API 实操、四类容器的选择,以及每条消息"从分配到释放"的生命周期纪律。

一、先看旧容器的别扭之处

JDK ByteBuffer 的读写共用一根指针:写完要读,必须先 flip() 把 limit 设到写入位置、position 归零;读完想再写,又要 clear()compact()。忘一次 flip,读出来全是空;在 Netty 这种"层层加工"的场景里,每次工位交接都要翻转指针,出错概率随工位数线性上升。

ByteBuf 的解法是把读写拆成两根指针:readerIndexwriterIndex,中间是可读区域,各自独立前进,互不需要翻转。再加上容量自动扩容、支持池化与堆外,它就是为流水线而生的容器。

双指针结构

双指针结构

二、日常操作速览

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 都替你记账了,只有"拿出去用"时才需要自己动账本。

本节要点回顾

  • 双指针取代 flip:readerIndex 与 writerIndex 独立前进,交接零翻转。
  • 容器家族:堆内、堆外、池化、非池化,按数据量与生命周期选。
  • 引用计数即所有权:retain 加账、release 销账,归零回收。
  • 传播即转让:fireChannelRead 把所有权移交下游,透传工位无需释放。
  • Simple 系自动释放:终点工位优先选 SimpleChannelInboundHandler。
  • 跨出流水线必须 retain:异步、缓存、集合持有都要配对记账,否则轻则异常重则泄漏。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U