什么是内存映射 (mmap)?
内存映射(Memory Mapped File)是一种操作系统提供的机制,它将磁盘上的文件直接映射到进程的虚拟内存地址空间中。
在 Java 中,通过 FileChannel.map() 方法可以创建一个 MappedByteBuffer。对这个 Buffer 的读写操作,会直接反映到磁盘文件上,就像操作内存数组一样简单,而不需要调用传统的 read() 或 write() 方法。
这样的话,数据实际上并没有被复制到 Java 堆内存中,而是直接在操作系统的 Page Cache 中进行读写,从而实现了“零拷贝”。
“零拷贝”简单来说就是CPU参与的数据拷贝次数为零次,从而大幅提升数据传输效率 – 因为每次数据从系统内存拷贝至程序内存都需要 CPU 参与,而 CPU 是非常宝贵的资源。
为什么它比传统 IO 快? (底层原理)
要理解 mmap 的优势,我们需要先了解传统 IO 是如何工作的。
传统 IO 的性能瓶颈
当我们使用 FileInputStream 读取文件时,数据需要经过两次拷贝:
- 磁盘 -> 内核缓冲区 (Page Cache):操作系统通过 DMA 将数据从磁盘读取到内核空间的 Page Cache 中。
- 内核缓冲区 -> 用户缓冲区 (Java Heap):CPU 将数据从内核空间拷贝到用户空间的堆内存中。
这个过程不仅涉及两次数据拷贝,还涉及两次上下文切换(用户态 <-> 内核态),这在处理大文件时会消耗大量 CPU 资源。
mmap 的工作原理 (零拷贝)
mmap 建立映射后,操作系统并不会立即读取文件数据,而是建立了一个“虚拟地址”到“文件内容”的映射关系。
- 建立映射:调用
map()时,操作系统修改进程的页表,将一段虚拟内存地址映射到文件的 Page Cache 上。此时物理内存中可能还没有数据。 - 缺页中断:当你第一次访问这段内存(例如调用
buffer.get())时,CPU 发现对应的物理页不存在,触发“缺页中断”。 - 懒加载:操作系统捕获中断,直接将文件数据从磁盘读取到 Page Cache 中。
- 直接访问:进程直接访问 Page Cache 中的数据。
优势总结:
- 零拷贝:数据只存在于 Page Cache 中,不需要拷贝到 Java 堆内存。
- 减少切换:读写操作直接在用户态进行,减少了系统调用带来的上下文切换。
Java 代码实现
基础用法
1 | RandomAccessFile raf = new RandomAccessFile("data.dat", "rw"); |
进程间通信 (IPC) 示例
mmap 非常适合用于进程间通信。以下是一个简单的 Writer/Reader 模型。
Writer (写入者)
注意:为了防止 Reader 读到不完整的数据,我们应该先写数据,最后写长度。
1 | public class ZeroCopyWriter { |
Reader (读取者)
1 | public class ZeroCopyReader { |