Skip to content

Netty ByteBuf

ByteBuf 是 Netty 的字节容器,比 JDK ByteBuffer 更适合网络编程。它用读写指针区分已读、可读、可写区域,避免频繁 flip

为什么 Netty 要自己设计 ByteBuf

JDK ByteBuffer 只有一个位置指针 position,读写切换时经常需要 flip()clear()compact()。对初学者来说,一旦切换顺序写错,就会读不到数据、覆盖数据或把旧数据当新数据。

Netty 的 ByteBuf 把读和写拆成两个指针:

  1. readerIndex 只负责下一次从哪里读。
  2. writerIndex 只负责下一次写到哪里。
  3. 两个指针之间就是可读数据。
  4. writerIndexcapacity 之间就是可写空间。

这样设计解决的是网络编程里的核心问题:数据可能分多次到达,业务也可能分多次读取。读写指针分离后,未读完的数据可以继续留在缓冲区里,后续数据到达时再拼接处理。

内存布局

mermaid
flowchart TD
    A["discardable bytes<br/>已读可丢弃区域"] --> B["readable bytes<br/>readerIndex 到 writerIndex"]
    B --> C["writable bytes<br/>writerIndex 到 capacity"]
    C --> D["capacity<br/>当前容量边界"]
指针含义
readerIndex下一次读取的位置
writerIndex下一次写入的位置
capacity当前容量
maxCapacity最大可扩容容量

核心原理:读写指针如何移动

mermaid
flowchart TD
    A["创建 ByteBuf"] --> B["writerIndex = 0<br/>readerIndex = 0"]
    B --> C["writeBytes 写入数据"]
    C --> D["writerIndex 向后移动"]
    D --> E["readBytes 读取数据"]
    E --> F["readerIndex 向后移动"]
    F --> G{"已读区域是否可丢弃"}
    G -- "是" --> H["discardReadBytes 回收已读空间"]
    G -- "否" --> I["继续等待或继续读取"]

如果不理解这套指针,很容易出现两类问题:

  1. 读取前不判断 readableBytes(),半包时读越界。
  2. 写入大包前不关注容量和最大容量,异常客户端可能撑爆内存。

堆内存与直接内存

类型特点场景
Heap ByteBufJVM 堆内,创建成本低业务处理、需要访问数组
Direct ByteBuf堆外内存,减少拷贝高吞吐网络 IO
Composite ByteBuf多个 Buffer 组合零拷贝拼接响应

引用计数

Netty 使用引用计数管理部分 ByteBuf。当引用计数归零后,内存可以释放。最常见的内存泄漏原因是收到 ByteBuf 后没有释放,或者异步使用时提前释放。

mermaid
flowchart TD
    A[收到 ByteBuf] --> B{是否继续传递给下一个Handler}
    B -- 是 --> C[由后续Handler负责释放]
    B -- 否 --> D[业务读取数据]
    D --> E[finally中 release]

引用计数的本质是“谁持有,谁负责释放”。如果一个 Handler 消费了消息但不再往后传,就要释放;如果还要交给下一个 Handler,就让后面的处理器接手;如果要跨线程异步使用,就要先 retain(),避免当前线程结束后对象被释放。

如果不会这样做,后果通常不是立刻报错,而是运行一段时间后堆外内存持续上涨,最后出现 OutOfDirectMemoryError 或频繁 Full GC。

开发建议

  1. 入站处理器如果消费了消息且不继续向后传递,需要释放。
  2. 使用 SimpleChannelInboundHandler 时,默认会自动释放读取到的消息。
  3. 异步线程中使用 ByteBuf 前要 retain,使用完再 release
  4. 开发环境打开泄漏检测,方便定位未释放的调用栈。

代码 Demo:读写 ByteBuf

java
ByteBuf buffer = Unpooled.buffer(16);

buffer.writeInt(100);
buffer.writeBytes("ok".getBytes(StandardCharsets.UTF_8));

int number = buffer.readInt();
byte[] bytes = new byte[buffer.readableBytes()];
buffer.readBytes(bytes);

System.out.println(number);
System.out.println(new String(bytes, StandardCharsets.UTF_8));

引用计数释放示例:

java
ByteBuf buf = ByteBufAllocator.DEFAULT.directBuffer();
try {
    buf.writeBytes("hello".getBytes(StandardCharsets.UTF_8));
    // 读取或处理数据
} finally {
    ReferenceCountUtil.release(buf);
}

如果使用 SimpleChannelInboundHandler<ByteBuf>,默认会在 channelRead0 执行完后自动释放消息。