Skip to content

内存映射文件

你有没有遇到过这个问题:

一个 10GB 的日志文件,你只想修改其中的第 5GB 那一行。用普通 FileInputStream 需要怎么读?

答案是:先读 5GB,跳过它,才能到那一行。

有没有更好的方式?内存映射文件就是答案。

为什么大文件随机访问慢

传统的 read/write 需要:

  1. 从磁盘读数据到内核缓冲区
  2. 从内核缓冲区拷贝到用户空间
  3. 每次读写都要经过这个流程

如果文件很大但只需要访问一小部分,传统 IO 会有大量无效的数据传输。

内存映射:像操作数组一样操作文件

FileChannel.map() 把文件直接映射到内存,程序像操作内存一样操作文件:

java
public static void processMappedFile(String path) throws IOException {
    try (RandomAccessFile raf = new RandomAccessFile(path, "rw");
         FileChannel channel = raf.getChannel()) {
        long fileSize = channel.size();
        MappedByteBuffer buffer = channel.map(
            FileChannel.MapMode.READ_WRITE, 0, fileSize);
        // 像操作内存数组一样操作文件
        while (buffer.hasRemaining()) {
            buffer.put(buffer.get());
        }
    }
}

映射后,buffer.get()buffer.put() 直接操作磁盘文件,没有任何系统调用——只有第一次访问时触发 page fault,把数据加载到内存。

MapMode:三种映射模式

模式说明
READ_ONLY只读映射,更改不生效
READ_WRITE读写映射,更改自动同步到磁盘
PRIVATE私有映射,更改不写回磁盘(写时复制)
java
// 只读映射
MappedByteBuffer readOnly = channel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize);

// 读写映射
MappedByteBuffer readWrite = channel.map(FileChannel.MapMode.READ_WRITE, 0, fileSize);

// 私有映射(写时复制)
MappedByteBuffer privateMap = channel.map(FileChannel.MapMode.PRIVATE, 0, fileSize);

适合场景

  • 大文件随机读写:比如修改日志文件的某一行
  • 高性能数据处理:数据库索引文件、游戏资源文件
  • 需要反复访问同一块数据:避免重复 IO

超大文件:分段映射

内存映射受 OS 页面大小限制(通常 4KB),单个映射不能超过 2GB。

对于超大文件,需要分段映射:

java
public static void processLargeFile(String path) throws IOException {
    try (RandomAccessFile raf = new RandomAccessFile(path, "rw");
         FileChannel channel = raf.getChannel()) {
        long fileSize = channel.size();
        long position = 0;
        long chunkSize = 1024L * 1024 * 1024; // 1GB 每段

        while (position < fileSize) {
            long remaining = fileSize - position;
            long size = Math.min(remaining, chunkSize);
            MappedByteBuffer buffer = channel.map(
                FileChannel.MapMode.READ_WRITE, position, size);
            // 处理 buffer
            while (buffer.hasRemaining()) {
                buffer.put(buffer.get());
            }
            position += size;
        }
    }
}

性能对比

方式适用场景性能
普通 read/write顺序读写
Buffered 读写频繁小读写更好
内存映射大文件随机读写最好

注意事项

  1. 映射大小受 OS 限制:单个映射通常不超过 2GB
  2. 更改自动同步:READ_WRITE 模式的修改会自动写回磁盘
  3. 关闭 Channel 后映射失效
  4. 内存占用高:映射的大小直接影响物理内存占用
┌─────────────────────────────────────────────────────────────────┐
│  内存映射的本质:把文件的一部分「变成」内存                        │
│                                                                 │
│  优点:随机访问极快,无传统 IO 的系统调用开销                     │
│  缺点:占用物理内存,不适合小型文件                               │
│                                                                 │
│  适用:大文件随机访问 + 高性能场景                                │
└─────────────────────────────────────────────────────────────────┘

基于 VitePress 构建