9.Netty的零拷贝

总结
  • Netty 的零拷贝分两类:OS 级别(FileRegion)和用户态级别(DirectBuffer、CompositeByteBuf、wrappedBuffer、slice)
  • 用户态零拷贝的核心思路:逻辑合并/切分,底层共享同一块内存,不做实际数据搬运
  • OS 级别零拷贝原理见 Linux中的零拷贝技术

1. 为什么需要用户态零拷贝?

OS 级别的零拷贝(sendfile)解决的是内核态的数据搬运问题,但 JVM 内部还有一层:

JVM 堆内存 → CPU 拷贝 → 堆外内存 → 系统调用 → 内核

执行 I/O 系统调用时,OS 不认识 JVM 堆内存(GC 随时可能移动对象),必须先把数据拷贝到堆外内存才能调用。Netty 在用户态做了 5 处优化来减少这类拷贝。

2. 堆外内存(DirectBuffer)

Netty 的 I/O 读写全部使用堆外内存(DirectBuffer),数据直接在堆外操作,省掉了堆内 → 堆外这一次 CPU 拷贝。

代价是堆外内存不受 GC 管理,需要手动释放,Netty 用引用计数(ReferenceCounted)来管理生命周期。

3. CompositeByteBuf(逻辑合并)

需要把 header 和 body 合并成一个完整报文时,传统做法要分配新内存再拷贝两次:

// 传统做法:2 次 CPU 拷贝
ByteBuf httpBuf = Unpooled.buffer(header.readableBytes() + body.readableBytes());
httpBuf.writeBytes(header);  // 拷贝 1
httpBuf.writeBytes(body);    // 拷贝 2

CompositeByteBuf 只是把多个 ByteBuf 的引用组合在一起,底层 byte 数组不动:

// 零拷贝:不发生内存拷贝
CompositeByteBuf httpBuf = Unpooled.compositeBuffer();
httpBuf.addComponents(true, header, body);

bytebuf示意

内部每个 Component 记录的是偏移量,读取时按偏移量定位到对应的原始 ByteBuf:

private static final class Component {
    final ByteBuf srcBuf;   // 原始 ByteBuf
    final ByteBuf buf;      // 去除包装后的 ByteBuf
    int offset;             // 相对于 CompositeByteBuf 的起始位置
    int endOffset;          // 相对于 CompositeByteBuf 的结束位置
    int srcAdjustment;      // CompositeByteBuf 起始索引相对于 srcBuf 读索引的偏移
    int adjustment;         // CompositeByteBuf 起始索引相对于 buf 读索引的偏移
}

CompositeByteBuf2

读取过程中各 Component 的偏移量变化示意(header 读 1 字节,body 读 2 字节):

CompositeByteBuf offset

4. Unpooled.wrappedBuffer(包装,不拷贝)

把已有的 byte[]ByteBufByteBuffer 包装成 ByteBuf,不产生内存拷贝:

byte[] bytes = ...;
ByteBuf buf = Unpooled.wrappedBuffer(bytes);  // 零拷贝包装

也是创建 CompositeByteBuf 的另一种推荐方式:

ByteBuf composite = Unpooled.wrappedBuffer(header, body);

Unpooled

5. ByteBuf.slice(切分,不拷贝)

wrappedBuffer 方向相反,把一个 ByteBuf 切成多个视图,底层共享同一块内存:

ByteBuf httpBuf = ...;
ByteBuf header = httpBuf.slice(0, 6);   // 前 6 字节,零拷贝
ByteBuf body   = httpBuf.slice(6, 4);   // 后 4 字节,零拷贝

ByteBuf.slice

注意:slice 出来的 ByteBuf 和原始 ByteBuf 共享内存,修改其中一个会影响另一个。

6. FileRegion(OS 级零拷贝)

文件传输场景,FileRegion 封装了 FileChannel.transferTo(),底层走 Linux 的 sendfile,数据直接从内核缓冲区传到网卡,不经过用户态:

FileRegion region = new DefaultFileRegion(fileChannel, 0, fileLength);
ctx.writeAndFlush(region);

OS 级零拷贝的原理见 [[Linux中的零拷贝技术]]。

7. 汇总对比

技术 类型 解决的问题 原理
DirectBuffer 用户态 堆内 → 堆外拷贝 I/O 直接用堆外内存
CompositeByteBuf 用户态 多 buffer 合并拷贝 逻辑合并,共享底层数组
wrappedBuffer 用户态 byte[] 转 ByteBuf 拷贝 包装引用,不复制数据
slice 用户态 ByteBuf 切分拷贝 共享底层数组,偏移量定位
FileRegion OS 级 内核态文件传输拷贝 sendfile,数据不经过用户态