Netty 的 ByteBuf 为什么好用?
结论先行:ByteBuf 用读写双指针取代了 ByteBuffer 的 flip() 状态机,支持按需扩容、池化分配、引用计数与堆内/堆外双实现,还能零拷贝切片。它把“缓冲区管理”这件容易出错的事,收敛成 readerIndex 与 writerIndex 两个数字。
一、与 ByteBuffer 的对比
| 维度 | ByteBuffer | ByteBuf |
|---|
| 读写切换 | 需要 flip(),忘了就出错 | 双指针,读写互不干扰 |
| 扩容 | 固定容量,满了只能新建 | ensureWritable() 自动扩容 |
| 池化 | 无 | PooledByteBufAllocator |
| 内存释放 | 依赖 GC | 引用计数,可主动释放 |
| 切片 | slice() 但索引独立 | slice() 共享内存,可选 retain |
| 复合 | 需自己拼接 | CompositeByteBuf 逻辑合并 |
| API | 相对底层 | readInt()、writeBytes() 等链式方法 |
二、双指针模型
+--------------------------------------------------+
| discardable | readable | writable |
+--------------------------------------------------+
0 readerIndex writerIndex capacity
readableBytes() = writerIndex - readerIndex
writeXxx() → writerIndex 右移
readXxx() → readerIndex 右移
discardReadBytes() → 把可读区左移,回收已读空间
clear() → 两个指针都归零,不清数据
ByteBuf buf = ctx.alloc().buffer(8);
buf.writeInt(100); // writerIndex: 0 → 4
buf.writeByte(1); // writerIndex: 4 → 5
int v = buf.readInt(); // readerIndex: 0 → 4,返回 100
byte b = buf.readByte(); // readerIndex: 4 → 5,返回 1
buf.clear(); // 复用,不清零内存
因为读写指针独立,ByteBuf 不需要 flip():写完直接交给下游读,读到哪算到哪。
三、扩容与池化
// 演示扩容:初始 4 字节,写入 10 字节自动扩到 16
ByteBuf buf = Unpooled.buffer(4);
buf.writeBytes(new byte[10]);
System.out.println(buf.capacity()); // 64:不足 64 时按 64 起步,之后每次翻倍
// 池化:复用内存,减少 GC 与分配开销
ByteBuf pooled = PooledByteBufAllocator.DEFAULT.directBuffer(1024);
try {
pooled.writeBytes(data);
channel.writeAndFlush(pooled.retain()); // 交给 Netty 后由 pipeline 释放
} finally {
pooled.release(); // 自己这份引用要还
}
四、引用计数
分配 ──> refCnt=1
retain() ──> refCnt=2
release() ──> refCnt=1 (仍可使用)
release() ──> refCnt=0 (归还池,禁止再访问)
| 方法 | 是否增加引用 | 谁负责释放 |
|---|
slice() | 否 | 父 buffer 的持有者 |
retainedSlice() | 是 | 调用方 |
duplicate() | 否 | 父 buffer 的持有者 |
writeAndFlush(buf) | 否(转移所有权) | pipeline 末端 |
五、选堆内还是堆外
| 类型 | 分配成本 | 读写速度 | 适用 |
|---|
HeapByteBuf | 低 | 快(JVM 可直接访问数组) | 业务处理、需要数组操作 |
DirectByteBuf | 高(需池化) | 快(少一次拷贝到内核) | 网络读写、大报文 |
常见追问
- 追问:忘了 release 会怎样?池化内存不归还,表现为堆外内存持续增长直到 OOM,用
-Dio.netty.leakDetection.level=paranoid 定位。
- 追问:
discardReadBytes() 什么时候用?需要复用已读空间时用,但它会移动内存,频繁调用反而降低性能。
- 追问:
ByteBuf 线程安全吗?不安全。它靠 EventLoop 串行访问保证安全,跨线程共享必须自己做同步或复制。
小结
- 双指针消除
flip(),读写不再互相干扰。
- 自动扩容 + 池化 + 引用计数,是它优于
ByteBuffer 的核心。
slice/duplicate 共享内存,retainedSlice 才需要自己释放。
- 堆外 + 池化适合网络读写,堆内适合业务处理。