Netty ByteBuf
ByteBuf 是 Netty 的字节容器,比 JDK ByteBuffer 更适合网络编程。它用读写指针区分已读、可读、可写区域,避免频繁 flip。
为什么 Netty 要自己设计 ByteBuf
JDK ByteBuffer 只有一个位置指针 position,读写切换时经常需要 flip()、clear()、compact()。对初学者来说,一旦切换顺序写错,就会读不到数据、覆盖数据或把旧数据当新数据。
Netty 的 ByteBuf 把读和写拆成两个指针:
readerIndex只负责下一次从哪里读。writerIndex只负责下一次写到哪里。- 两个指针之间就是可读数据。
writerIndex到capacity之间就是可写空间。
这样设计解决的是网络编程里的核心问题:数据可能分多次到达,业务也可能分多次读取。读写指针分离后,未读完的数据可以继续留在缓冲区里,后续数据到达时再拼接处理。
内存布局
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["继续等待或继续读取"]如果不理解这套指针,很容易出现两类问题:
- 读取前不判断
readableBytes(),半包时读越界。 - 写入大包前不关注容量和最大容量,异常客户端可能撑爆内存。
堆内存与直接内存
| 类型 | 特点 | 场景 |
|---|---|---|
| Heap ByteBuf | JVM 堆内,创建成本低 | 业务处理、需要访问数组 |
| 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。
开发建议
- 入站处理器如果消费了消息且不继续向后传递,需要释放。
- 使用
SimpleChannelInboundHandler时,默认会自动释放读取到的消息。 - 异步线程中使用
ByteBuf前要retain,使用完再release。 - 开发环境打开泄漏检测,方便定位未释放的调用栈。
代码 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 执行完后自动释放消息。
