什么是零拷贝?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
I/O 底层原理:面试官想知道你是否清楚一次普通的读写操作,数据在内核态和用户态之间到底经历了什么:DMA 拷贝、CPU 拷贝、上下文切换,这几个概念能不能串成一条线。
-
性能优化思维:零拷贝本质是“减少无效搬运”。考察你遇到 I/O 密集型场景时,有没有从操作系统层面去思考优化,而不是只会加缓存加机器。
-
技术视野:Kafka 为什么快、Netty 的零拷贝指什么,这些追问都在试探你是“背概念”还是“真理解”。
核心答案
先给结论:零拷贝说的是一类 I/O 优化技术:数据传输过程中,尽量减少甚至完全消除 CPU 参与的数据拷贝(理想情况下一次都不需要),顺便把不必要的内核态与用户态上下文切换也省掉。
注意,零拷贝不是“零次拷贝”,而是“CPU 不需要参与搬运数据”。DMA 拷贝还是免不了的——数据总得从磁盘到内存吧?只要这段搬运不需要 CPU 出力,就算零拷贝。
直接对比一下各种方案:
| 方案 | 拷贝次数 | 上下文切换 | CPU 拷贝 |
|---|---|---|---|
传统 read + write |
4 次 | 4 次 | 2 次 |
mmap + write |
3 次 | 4 次 | 1 次 |
sendfile |
3 次 | 2 次 | 1 次 |
sendfile + SG-DMA |
2 次 | 2 次 | 0 次 ✅ |
深度解析
一、传统 I/O 有多浪费:4 次拷贝 + 4 次切换
以“读取磁盘文件并通过网卡发送出去”这个经典场景为例(比如你写了一个文件下载服务),传统写法是:
// 传统 I/O:先把文件读进来,再写出去
File file = new File("data.txt");
RandomAccessFile raf = new RandomAccessFile(file, "r");
byte[] buffer = new byte[1024];
// 1. 读:数据从内核缓冲区拷贝到用户缓冲区
raf.read(buffer);
// 2. 写:数据从用户缓冲区拷贝到 socket 缓冲区
socket.getOutputStream().write(buffer);
看起来就一次读一次写,但操作系统在背后干的活儿多得多:
完整走一遍这条链路:
- 第一次拷贝(DMA):磁盘上的数据通过 DMA 引擎拷贝到内核的页缓存(Page Cache)里。这一步不占 CPU,DMA 芯片自己搬。
- 第二次拷贝(CPU):
read()系统调用返回前,CPU 把数据从内核缓冲区拷贝到用户空间的缓冲区。这一步跨过了内核态到用户态的边界。 - 第三次拷贝(CPU):调用
write(),CPU 又把数据从用户缓冲区拷贝回内核的 Socket 缓冲区。 - 第四次拷贝(DMA):DMA 引擎把数据从 Socket 缓冲区拷贝到网卡,真正发出去。
同时,read() 和 write() 各对应两次上下文切换(用户态 → 内核态 → 用户态),加起来 4 次拷贝 + 4 次上下文切换。
问题在哪?你品品第二、三次拷贝——数据从内核搬到用户空间,又原封不动搬回内核。用户程序压根没修改这个文件的内容,纯粹是“白跑两趟”。文件越大、请求越多,这种无效搬运的浪费就越夸张。
二、优化思路一:mmap + write,砍掉一次 CPU 拷贝
既然用户程序不需要碰数据内容,那能不能让用户空间和内核空间共享同一块内存?这就是 mmap(内存映射)。
// Java NIO 中用 MappedByteBuffer 使用 mmap
FileChannel channel = new RandomAccessFile(file, "r").getChannel();
MappedByteBuffer mapped = channel.map(
FileChannel.MapMode.READ_ONLY, 0, file.length());
mmap 把内核页缓存直接映射到用户空间,用户程序操作这块内存,实际操作的就是页缓存本身,省掉了“内核 → 用户”的那次 CPU 拷贝。流程变成:
- 第一次(DMA):磁盘 → 页缓存,不变。
- 第二次(CPU):
write()时 CPU 把数据从共享的页缓存拷贝到 Socket 缓冲区。 - 第三次(DMA):Socket 缓冲区 → 网卡。
拷贝次数从 4 降到 3,CPU 拷贝从 2 次降到 1 次。不过上下文切换还是 4 次(mmap 和 write 也各两次)。
顺带一提,RocketMQ 就大量使用了 mmap,CommitLog 文件写入走的就是内存映射这条路。
三、优化思路二:sendfile,连用户空间都不用去了
Linux 2.2 内核引入了 sendfile 系统调用,专门为“文件到 Socket”这种传输场景设计,数据全程在内核态流转,根本不经过用户空间:
对比 mmap + write,拷贝次数一样是 3 次,但因为只有一次系统调用,上下文切换从 4 次降到 2 次。
Java NIO 对应的 API 是 FileChannel.transferTo(),底层在 Linux 上就是调用 sendfile:
// 零拷贝发送文件:一行搞定
fileChannel.transferTo(0, fileChannel.size(), socketChannel);
Kafka 高吞吐的功臣之一就是它——Broker 之间副本同步、消费者拉取消息,走的都是 transferTo 这条零拷贝路径,数据从页缓存直接飞到网卡,完全不经过 JVM 堆。这也是为什么 Kafka 的消息吞吐量能那么夸张。
四、终极形态:sendfile + SG-DMA,CPU 拷贝归零
Linux 2.4 内核之后,如果网卡支持 SG-DMA(Scatter-Gather DMA,分散-聚集 DMA)技术,sendfile 还能再进一步:
这时候 CPU 连那次“页缓存 → Socket 缓冲区”的拷贝都不用干了,只往 Socket 缓冲区里塞入描述符信息(数据在内存中的地址、长度),SG-DMA 引擎直接根据描述符把页缓存里的数据 gather 到网卡。
全程 2 次拷贝,全是 DMA 干的,CPU 拷贝次数为 0——这才是字面意义上的“零拷贝”,也是这个技术名字的由来。
五、别忘了 Netty 的“零拷贝”是另一回事
面试里特别爱挖这个坑。Netty 官方说自己有零拷贝,但 Netty 的零拷贝是缓冲区层面的零拷贝,跟操作系统那个不是一个概念:
| 对比 | OS 层零拷贝 | Netty 零拷贝 |
|---|---|---|
| 层面 | 内核态与用户态之间 | ByteBuf 对象之间 |
| 目的 | 减少 CPU 拷贝和上下文切换 | 减少堆内内存在各组件间的复制 |
| 典型实现 | sendfile、mmap |
CompositeByteBuf、slice()、duplicate() |
Netty 的几个典型操作:
CompositeByteBuf:把多个 ByteBuf 逻辑上拼成一个整体,避免内存合并时的真实拷贝;slice()/duplicate():切分出的新 ByteBuf 和原 Buf 共享同一块底层内存;Unpooled.wrappedBuffer():把 byte 数组包装成 ByteBuf,不发生数组复制。
当然,Netty 底层传输数据时也会用到 OS 层的零拷贝(比如 FileRegion 封装了 transferTo),两层零拷贝它都有,但面试时一定要分清楚层次,不然容易被打脸。
面试高频追问
-
追问一:零拷贝是不是万能的?什么场景不适合?
不是。零拷贝的前提是用户程序不需要处理数据内容(纯转发)。如果你拿到数据后还要做加密、压缩、校验,数据必须进用户空间,零拷贝就玩不转了。另外超大文件传输场景,页缓存反而可能被大文件“冲刷”掉影响其他热点数据,这种场景会考虑直接 I/O(绕过页缓存)配合异步 I/O。
-
追问二:Kafka 和 RocketMQ 为什么一个选 sendfile,一个选 mmap?
使用场景不同。Kafka 的核心路径是“消费者拉取消息 → 网络发送”,数据不需要在 Broker 端加工,
sendfile最合适;RocketMQ 的 CommitLog 写入是核心瓶颈,写入用mmap让磁盘和内存映射打通,读写都受益。各自的业务形态决定了技术选型。 -
追问三:mmap 一定比传统 read 快吗?
不一定。小文件、随机读、跨页访问场景下
mmap缺页中断(page fault)的代价可能更高,映射本身也有建立和销毁的开销。没有银弹,都得看场景。
常见面试变体
- “
mmap和sendfile的区别是什么?” - “Kafka 为什么快?”(零拷贝是必答点之一,配合顺序写、页缓存一起说)
- “Netty 的零拷贝体现在哪里?”
- “
transferTo方法了解吗?底层怎么实现的?”
记忆口诀
演进路线记一条线:传统四次拷贝太浪费 → mmap 省一次(共享内存)→ sendfile 少切换(一次调用)→ SG-DMA 零 CPU(网卡直取)。
数字对应记法:4 → 3 → 3 → 2(拷贝次数),4 → 4 → 2 → 2(切换次数),最后一步 CPU 拷贝归零,故称“零拷贝”。
总结
零拷贝的核心就一句话:数据在内核态流转时,CPU 别当搬运工。从传统 I/O 的 4 次拷贝 4 次切换,到 mmap 砍一次拷贝,再到 sendfile 砍两次切换,最后 SG-DMA 让 CPU 拷贝彻底归零——把这条演进线讲清楚,再带上 Kafka、RocketMQ、Netty 的落地例子,这道题你就答出区分度了。
