Netty零拷贝核心是FileRegion触发sendfile、CompositeByteBuf逻辑聚合、slice共享视图;堆外内存不等于零拷贝,仅提升效率。

用 FileRegion 触发 sendfile 系统调用,跳过用户态内存
Netty 的文件传输零拷贝,核心是绕过 JVM 堆内存参与。关键不是你自己写 read() + write(),而是让内核直接把磁盘数据送进 socket 发送缓冲区。这靠的是 FileRegion —— 它本身不持有数据,只封装了 FileChannel 和偏移/长度信息,底层最终调用 Linux 的 sendfile(2) 系统调用。
常见错误是误用 Unpooled.wrappedBuffer() 包装 byte[] 再写入 channel,这仍是用户态内存中转;或者用 RandomAccessFile.getChannel().read(ByteBuffer) 读到堆内 buffer,再 write,等于白忙活。
- 必须用
FileChannel(不能是InputStream)配合DefaultFileRegion - 目标
Channel必须支持transferTo(如NioSocketChannel) - 文件需打开为只读(
"r"),且FileChannel不可被其他线程关闭或重复使用
示例:
RandomAccessFile raf = new RandomAccessFile("log.bin", "r");
FileChannel channel = raf.getChannel();
ctx.write(new DefaultFileRegion(channel, 0, channel.size()));
用 CompositeByteBuf 合并多个 ByteBuf,避免拼接拷贝
当你要把 header、body、footer 三个独立的 ByteBuf 一起发送时,传统做法是分配一个大 buffer,再逐个 readBytes() 拷过去。而 CompositeByteBuf 是逻辑聚合:它内部维护一个组件数组,readableBytes()、getByte() 等操作自动路由到对应子 buffer,全程无内存复制。
注意它不是万能的:如果后续代码调用了 array() 或强制转成 byte[],就会触发真实合并(即拷贝);另外某些 handler(比如基于 ByteBuffer 的旧式编解码器)可能不兼容。
- 创建方式:用
Unpooled.compositeBuffer(),不要 new - 添加组件用
addComponents(true, buf1, buf2),true表示自动释放原 buffer 引用 - 发送后记得
release(),否则组件 buffer 不会被回收
用 slice() 和 duplicate() 复用底层存储,而非复制数据
当你从一个大 ByteBuf 中截取一段发出去(比如 HTTP body),别用 copy()。调用 slice() 返回的是共享同一片内存的新 ByteBuf,只是调整了 readerIndex 和 writerIndex;duplicate() 更进一步,连引用计数都共享。
但要注意生命周期管理:如果原 buffer 被 release() 了,所有 slice 都会失效;若需长期持有某段,应改用 retainedSlice()(增加引用计数)。
-
slice():适合短期、与原 buffer 生命周期一致的场景(如 pipeline 中传递) -
retainedSlice():适合需要独立管理生命周期的子段(如异步写入磁盘) - 避免对
slice()调用capacity()或maxCapacity(),它们返回的是原 buffer 的值,容易误判
为什么 Unpooled.directBuffer() 不等于零拷贝?
很多人以为用了堆外内存(directBuffer)就是零拷贝,这是误解。堆外内存只是避免了 JVM 堆 GC 压力,并没跳过内核拷贝。真正减少拷贝次数的,是上面三种机制:FileRegion 触发 sendfile、CompositeByteBuf 逻辑聚合、slice 共享视图。堆外内存只是让这些操作更高效——比如 FileRegion 在使用 direct buffer 时,DMA 可直接寻址,省掉一次内核内部拷贝。
所以如果你的应用大量做小包拼装、频繁读文件、或需拆分大 buffer,重点不在“是不是 direct”,而在“有没有绕过 copy”。否则即使全用 directBuffer,该拷贝还是拷贝。










