知识模块
☕ Java 知识模块
五、Java IO/NIO
零拷贝

零拷贝

面试提问

"什么是零拷贝?Java NIO 如何实现零拷贝?"


传统 IO 的数据拷贝

传统读取并发送文件

// 传统方式
File file = new File("test.txt");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
byte[] arr = new byte[(int) file.length()];
raf.read(arr);  // 读取到用户空间
socket.getOutputStream().write(arr);  // 发送出去

数据拷贝流程(4 次拷贝,4 次上下文切换)

┌──────────────────────────────────────────────────────┐
│              传统 IO 数据流                           │
├──────────────────────────────────────────────────────┤
│                                                      │
│  ┌─────────┐    DMA拷贝     ┌─────────┐             │
│  │  磁盘   │ ─────────────→ │ 内核缓冲 │             │
│  └─────────┘      (1)       └────┬────┘             │
│                                     │ CPU拷贝(2)     │
│                                     ↓                │
│                              ┌─────────┐             │
│                              │ 用户缓冲 │             │
│                              └────┬────┘             │
│                                     │ CPU拷贝(3)     │
│                                     ↓                │
│                              ┌─────────┐             │
│                              │Socket缓冲│             │
│                              └────┬────┘             │
│                                     │ DMA拷贝(4)     │
│                                     ↓                │
│                              ┌─────────┐             │
│                              │  网卡   │             │
│                              └─────────┘             │
│                                                      │
│  上下文切换:用户态→内核态→用户态→内核态→用户态 (4次)  │
└──────────────────────────────────────────────────────┘

性能问题

问题说明
多次拷贝4 次数据拷贝,2 次 CPU 参与
多次切换4 次上下文切换,开销大
用户空间参与数据在用户空间无意义的中转

零拷贝技术

零拷贝(Zero Copy)不是完全没有拷贝,而是减少 CPU 参与的拷贝次数

Linux 零拷贝方式

方式系统调用说明
mmapmmap()内存映射文件
sendfilesendfile()内核态直接传输
splicesplice()管道传输
teetee()复制管道数据

mmap(内存映射)

原理

将文件映射到用户空间的虚拟内存,省去内核缓冲区到用户缓冲区的拷贝。

┌──────────────────────────────────────────────────────┐
│              mmap 数据流                              │
├──────────────────────────────────────────────────────┤
│                                                      │
│  ┌─────────┐    DMA拷贝     ┌─────────┐             │
│  │  磁盘   │ ─────────────→ │ 内核缓冲 │             │
│  └─────────┘      (1)       └────┬────┘             │
│                                     │                │
│                              ┌──────┴──────┐         │
│                              │ 用户虚拟内存 │ ←映射    │
│                              └──────┬──────┘         │
│                                     │ CPU拷贝(2)     │
│                                     ↓                │
│                              ┌─────────┐             │
│                              │Socket缓冲│             │
│                              └────┬────┘             │
│                                     │ DMA拷贝(3)     │
│                                     ↓                │
│                              ┌─────────┐             │
│                              │  网卡   │             │
│                              └─────────┘             │
│                                                      │
│  拷贝次数:3 次(减少 1 次 CPU 拷贝)                  │
└──────────────────────────────────────────────────────┘

Java 实现

FileChannel channel = new RandomAccessFile("test.txt", "r").getChannel();
MappedByteBuffer buffer = channel.map(
    FileChannel.MapMode.READ_ONLY, 
    0, 
    channel.size()
);
// buffer 直接映射文件内容,无需 read() 调用

sendfile

原理

数据完全在内核态传输,不经过用户空间。

┌──────────────────────────────────────────────────────┐
│              sendfile 数据流                          │
├──────────────────────────────────────────────────────┤
│                                                      │
│  ┌─────────┐    DMA拷贝     ┌─────────┐             │
│  │  磁盘   │ ─────────────→ │ 内核缓冲 │             │
│  └─────────┘      (1)       └────┬────┘             │
│                                     │ CPU拷贝(2)     │
│                                     ↓                │
│                              ┌─────────┐             │
│                              │Socket缓冲│             │
│                              └────┬────┘             │
│                                     │ DMA拷贝(3)     │
│                                     ↓                │
│                              ┌─────────┐             │
│                              │  网卡   │             │
│                              └─────────┘             │
│                                                      │
│  拷贝次数:3 次(上下文切换只有 2 次)                 │
└──────────────────────────────────────────────────────┘

Java 实现

FileChannel fileChannel = new FileInputStream("test.txt").getChannel();
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8080));
 
// 零拷贝传输
fileChannel.transferTo(0, fileChannel.size(), socketChannel);

sendfile + DMA Scatter/Gather

原理(真正的零拷贝)

Linux 2.4+ 支持 DMA Scatter/Gather,数据描述符直接传给 Socket,DMA 直接从内核缓冲读取。

┌──────────────────────────────────────────────────────┐
│         sendfile + DMA Scatter/Gather                │
├──────────────────────────────────────────────────────┤
│                                                      │
│  ┌─────────┐    DMA拷贝     ┌─────────┐             │
│  │  磁盘   │ ─────────────→ │ 内核缓冲 │             │
│  └─────────┘      (1)       └────┬────┘             │
│                                     │                │
│                     描述符(位置、长度)传递           │
│                                     ↓                │
│                              ┌─────────┐             │
│                              │Socket缓冲│             │
│                              └────┬────┘             │
│                                     │ DMA拷贝(2)     │
│                                     ↓                │
│                              ┌─────────┐             │
│                              │  网卡   │             │
│                              └─────────┘             │
│                                                      │
│  拷贝次数:2 次 DMA 拷贝,CPU 不参与!                 │
│  上下文切换:2 次                                     │
└──────────────────────────────────────────────────────┘

Java NIO 零拷贝 API

FileChannel.transferTo()

// 最简单的零拷贝
try (FileChannel from = new FileInputStream("source.txt").getChannel();
     FileChannel to = new FileOutputStream("dest.txt").getChannel()) {
    
    long transferred = from.transferTo(0, from.size(), to);
}

FileChannel.transferFrom()

try (FileChannel from = new FileInputStream("source.txt").getChannel();
     FileChannel to = new FileOutputStream("dest.txt").getChannel()) {
    
    to.transferFrom(from, 0, from.size());
}

MappedByteBuffer

try (FileChannel channel = FileChannel.open(
        Paths.get("test.txt"), 
        StandardOpenOption.READ, 
        StandardOpenOption.WRITE)) {
    
    MappedByteBuffer buffer = channel.map(
        FileChannel.MapMode.READ_WRITE, 
        0, 
        channel.size()
    );
    
    // 直接修改映射内存
    buffer.put(0, (byte) 'H');
    // 修改会自动同步到文件
}

零拷贝应用场景

场景技术说明
Kafkasendfile高效消息传输
NettyFileRegion文件传输
Nginxsendfile静态文件服务
RocketMQmmap + sendfile消息存储

对比总结

方式CPU 拷贝DMA 拷贝上下文切换
传统 IO224
mmap124
sendfile122
sendfile+DMA022

面试要点总结

问题答案要点
什么是零拷贝?减少 CPU 参与的数据拷贝,降低上下文切换
传统 IO 几次拷贝?4 次(2 DMA + 2 CPU),4 次上下文切换
mmap 原理?文件映射到用户虚拟内存,省去一次 CPU 拷贝
sendfile 原理?内核态直接传输,用户空间不参与
transferTo 作用?Java NIO 提供的零拷贝方法

参考资料