第 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 存到列表里,等一分钟后再读?