网站公告

本次更新:没啥更新的

“钱塘江上潮信起,今日方知我是我”

Skip to content

零拷贝 ​

字数: 0 字 时长: 0 分钟

零拷贝(Zero Copy)是一种 I/O 优化技术。

它的核心目标并不是让数据真的“一次都不复制”,而是:

尽量减少数据在用户态和内核态之间的复制,以及减少 CPU 参与的数据搬运和上下文切换。

在 Java 中,零拷贝主要会接触到:

text
FileChannel.transferTo()
FileChannel.transferFrom()
MappedByteBuffer

底层则经常涉及:

text
mmap
sendfile

1. 用户态和内核态 ​

理解零拷贝之前,需要先知道:

text
用户态 User Space
内核态 Kernel Space

Java 程序运行在:

text
用户态

而磁盘、网络等硬件设备需要通过:

text
操作系统内核

进行访问。

简单来说:

text
Java 程序
    ↓
用户态
    ↓
System Call
    ↓
内核态
    ↓
磁盘 / 网卡

当程序进行文件或者网络 IO 时,就可能发生:

text
用户态
  ↕
内核态

之间的数据复制和状态切换。


2. 传统文件传输 ​

假设现在要把一个文件发送到网络:

text
磁盘文件
    ↓
网络

传统方式通常会写成:

java
FileInputStream input =
        new FileInputStream("test.txt");

Socket socket =
        new Socket("localhost", 8080);

OutputStream output =
        socket.getOutputStream();

byte[] buffer =
        new byte[8192];

int len;

while ((len = input.read(buffer)) != -1) {
    output.write(buffer, 0, len);
}

从 Java 代码上看,只是:

text
read()
    ↓
write()

但底层的数据传输过程要复杂得多。


3. 传统 IO 的数据复制过程 ​

典型情况下,数据可能经历这样的过程:

text
磁盘
 ↓
内核缓冲区
 ↓
用户缓冲区
 ↓
Socket 缓冲区
 ↓
网卡

可以画成:

text
            内核态                     用户态

磁盘
 ↓
Page Cache
    │
    │ CPU Copy
    └──────────────→ 用户 Buffer
                         │
                         │ CPU Copy
                         ↓
                    Socket Buffer
                         ↓
                        网卡

通常可以把它理解为:

text
磁盘
 ↓
内核 Page Cache
 ↓
用户态 Buffer
 ↓
内核 Socket Buffer
 ↓
网卡

这里既包含:

text
DMA Copy

也可能包含:

text
CPU Copy

传统 IO 最大的问题之一就是:

数据需要先从内核复制到用户空间,再从用户空间复制回内核空间。

也就是:

text
内核
 ↓
用户
 ↓
内核

中间这两次数据搬运,在很多场景下其实没有必要。


4. 上下文切换 ​

传统:

java
read()
write()

不仅会发生数据复制,还会发生用户态和内核态之间的切换。

例如调用:

java
read()

大致过程:

text
用户态
 ↓
read()
 ↓
内核态
 ↓
完成读取
 ↓
用户态

然后:

java
write()

又会发生一次类似过程。

所以:

text
read()
+
write()

会产生多次:

text
用户态 ↔ 内核态

上下文切换。

而零拷贝除了减少数据复制之外,通常也希望减少这种切换带来的开销。


5. 什么是零拷贝 ​

零拷贝的核心思想就是:

如果数据最终只是从磁盘发送到网络,就没有必要先复制到 Java 用户空间,再复制回操作系统内核。

传统方式:

text
磁盘
 ↓
内核
 ↓
Java 用户空间
 ↓
内核
 ↓
网卡

优化后,希望变成:

text
磁盘
 ↓
内核
 ↓
网卡

也就是说:

text
Java 程序

负责告诉操作系统:

把这段数据从这里传到那里。

而不需要真正把整块数据搬到 JVM 内存中再搬回去。


6. mmap ​

mmap 即:

text
Memory Mapped
内存映射

它可以把文件的一部分区域直接映射到进程的虚拟地址空间。

传统读取:

text
磁盘
 ↓
内核缓冲区
 ↓
用户 Buffer

使用 mmap 后:

text
文件
 ↓
内核 Page Cache
 ↕
用户虚拟地址空间映射

程序可以像访问内存一样访问文件内容。

这样可以减少一次:

text
内核缓冲区
    ↓
用户缓冲区

的数据复制。


7. Java 中的 MappedByteBuffer ​

Java NIO 中对应 mmap 的主要是:

java
MappedByteBuffer

通过:

java
FileChannel.map()

创建。

例如:

java
try (
        RandomAccessFile file =
                new RandomAccessFile(
                        "test.txt",
                        "rw"
                );

        FileChannel channel =
                file.getChannel()
) {

    MappedByteBuffer buffer =
            channel.map(
                    FileChannel.MapMode.READ_WRITE,
                    0,
                    channel.size()
            );

    System.out.println(
            (char) buffer.get(0)
    );
}

这里:

java
channel.map()

会把文件的一部分区域映射到内存。

之后可以直接通过:

java
MappedByteBuffer

访问。


8. MappedByteBuffer 的特点 ​

传统文件读取:

text
File
 ↓
Channel
 ↓
ByteBuffer
 ↓
程序

内存映射:

text
File
 ↓
MappedByteBuffer
 ↓
程序直接访问映射区域

它比较适合:

text
大文件
随机访问文件
需要频繁访问文件中的部分数据

但:

MappedByteBuffer 不等于“所有场景都更快”。

因为它也存在:

text
映射成本
页缺失 Page Fault
资源释放
虚拟内存占用

等问题。


9. sendfile ​

另外一种经典的零拷贝技术是:

text
sendfile

它主要用于:

把文件数据直接发送到网络 Socket。

传统方式:

text
磁盘
 ↓
Page Cache
 ↓
用户 Buffer
 ↓
Socket Buffer
 ↓
网卡

sendfile:

text
磁盘
 ↓
Page Cache
 ↓
Socket Buffer
 ↓
网卡

这里绕过了:

text
用户 Buffer

数据不需要进入 Java 用户空间。


10. sendfile 的进一步优化 ​

在支持相关硬件和操作系统优化的情况下,还可以进一步减少 CPU 数据复制。

大致可以理解为:

text
磁盘
 ↓
Page Cache
 ↓
DMA
 ↓
网卡

此时 CPU 更多只是负责:

text
描述数据在哪里
要发送多少数据

而不是亲自把大量文件内容:

text
复制来
复制去

具体的数据复制次数会受到:

text
操作系统
内核版本
硬件
DMA
网卡
JDK 实现

等因素影响。

因此不要死记:

text
零拷贝 = 固定 0 次复制

11. FileChannel.transferTo ​

Java NIO 中最典型的零拷贝 API:

java
FileChannel.transferTo()

例如:

java
try (
        FileChannel source =
                new FileInputStream(
                        "source.txt"
                ).getChannel();

        FileChannel target =
                new FileOutputStream(
                        "target.txt"
                ).getChannel()
) {

    source.transferTo(
            0,
            source.size(),
            target
    );
}

调用:

java
source.transferTo()

表示:

text
source
    ↓
target

数据可以直接在 Channel 之间传输。

不需要自己:

java
ByteBuffer.allocate()

然后:

java
read()
write()

12. FileChannel.transferFrom ​

还有:

java
transferFrom()

例如:

java
target.transferFrom(
        source,
        0,
        source.size()
);

方向:

text
source
    ↓
target

区别主要是调用者不同:

text
source.transferTo(target)

target.transferFrom(source)

可以简单记:

text
transferTo
    ↓
我把数据给别人

transferFrom
    ↓
我从别人那里拿数据

13. transferTo 和 sendfile ​

在支持的操作系统和 JDK 实现中:

java
FileChannel.transferTo()

可能利用操作系统提供的:

text
sendfile

等机制完成高效数据传输。

从 Java 程序的角度:

text
FileChannel
    ↓
transferTo()
    ↓
SocketChannel

可以避免自己手动:

text
FileChannel.read()
    ↓
ByteBuffer
    ↓
SocketChannel.write()

例如:

java
FileChannel fileChannel =
        new FileInputStream(
                "video.mp4"
        ).getChannel();

SocketChannel socketChannel =
        SocketChannel.open();

fileChannel.transferTo(
        0,
        fileChannel.size(),
        socketChannel
);

概念上:

text
传统:

File
 ↓
FileChannel
 ↓
ByteBuffer
 ↓
Java
 ↓
SocketChannel


transferTo:

File
 ↓
FileChannel
 ↓
操作系统优化
 ↓
SocketChannel

具体是否使用真正的底层零拷贝机制,仍然取决于:

text
操作系统
JDK
Channel 类型
具体实现

14. 传统 IO 与零拷贝对比 ​

传统 IO:

text
磁盘
 ↓
内核缓冲区
 ↓
用户缓冲区
 ↓
Socket 缓冲区
 ↓
网卡

零拷贝优化:

text
磁盘
 ↓
内核缓冲区
 ↓
Socket / 网卡

核心减少的是:

text
内核态
  ↓
用户态
  ↓
内核态

这种不必要的数据搬运。

简单对比:

对比传统 IO零拷贝
用户态参与较多较少
CPU 数据复制较多减少
上下文切换较多可以减少
JVM Buffer通常需要某些场景可绕过
大文件传输开销较大更有优势

15. 零拷贝真的是 0 次复制吗? ​

不是。

这是学习零拷贝最容易出现的误区。

所谓:

text
Zero Copy

更多是指:

尽量减少 CPU 参与的数据复制,特别是减少用户空间和内核空间之间的数据复制。

例如磁盘数据仍然需要:

text
磁盘
 ↓
内存

网卡发送数据也需要:

text
内存
 ↓
网卡

这些数据移动依然存在。

因此:

text
零拷贝
≠
物理意义上的 0 次数据复制

更准确的理解是:

text
减少无意义的数据复制
+
减少 CPU 参与
+
减少用户态 / 内核态切换

16. 零拷贝的应用场景 ​

零拷贝特别适合:

text
文件服务器
大文件传输
消息队列
网络框架
静态资源服务器

常见技术中也经常会接触到零拷贝相关设计:

text
Kafka
Netty

例如 Kafka 这类消息系统需要大量进行:

text
磁盘
 ↓
网络

的数据传输。

如果每条消息都需要:

text
磁盘
 ↓
内核
 ↓
JVM
 ↓
内核
 ↓
网卡

会产生大量额外的数据复制。

利用零拷贝思想,可以减少这部分开销。


17. mmap 和 sendfile 的区别 ​

两者虽然都属于常见的零拷贝优化方式,但应用场景不同。

mmap ​

text
文件
 ↓
映射到进程地址空间
 ↓
程序可以直接访问

更适合:

text
程序需要读取 / 修改文件内容
随机访问文件

Java:

java
MappedByteBuffer

sendfile ​

text
文件
 ↓
直接发送到 Socket

更适合:

text
程序本身不需要处理文件内容
只需要把文件发送出去

Java 中典型对应:

java
FileChannel.transferTo()

可以简单理解:

text
mmap
    ↓
我要访问这个文件的数据


sendfile
    ↓
我不关心数据内容
直接帮我发出去

18. Java 中的零拷贝 ​

Java 中主要记住:

text
MappedByteBuffer
    ↓
FileChannel.map()
    ↓
mmap 思想

以及:

text
FileChannel.transferTo()
FileChannel.transferFrom()
    ↓
Channel 之间直接传输
    ↓
某些平台可利用 sendfile 等底层优化

关系:

text
                Java 零拷贝
                    │
          ┌─────────┴─────────┐
          │                   │
   MappedByteBuffer      transferTo()
          │              transferFrom()
          │                   │
        mmap             sendfile 等优化

19. 小结 ​

传统 IO 传输文件时,数据可能经历:

text
磁盘
 ↓
内核
 ↓
用户空间
 ↓
内核
 ↓
网卡

其中:

text
内核
 ↓
用户
 ↓
内核

存在额外的数据复制。

零拷贝的核心思想就是:

让数据尽可能直接在内核、文件和网络设备之间传输,减少用户态参与的数据搬运。

Java 中主要关注:

text
MappedByteBuffer
    ↓
mmap


FileChannel.transferTo()
FileChannel.transferFrom()
    ↓
sendfile 等底层优化

最终可以记成:

text
传统 IO
    ↓
数据经过 JVM
    ↓
复制次数较多


零拷贝
    ↓
尽可能绕过 JVM 用户空间
    ↓
减少 CPU Copy
    ↓
减少上下文切换
    ↓
提高大数据传输效率

一句话理解:

零拷贝不是“完全不复制”,而是尽量不让数据在用户态和内核态之间来回搬运。

Noctiflorous Garden · MIT License