Skip to content

大文件处理优化

GB 级大文件不能一次性读入内存。

有人问过我:为什么 Files.readAllBytes() 读一个 10GB 的文件会 OOM?

答案是:它试图把 10GB 全部加载到内存。

这一节讲清楚如何高效处理大文件。

核心原则:分块处理

java
// ❌ 错误:一次性读入内存
byte[] data = Files.readAllBytes(Path.of("bigfile.dat")); // 内存爆炸

// ✅ 正确:分块读取
try (
    BufferedInputStream in = new BufferedInputStream(
        new FileInputStream("bigfile.dat"), 8 * 1024 * 1024) // 8MB 缓冲
) {
    byte[] buffer = new byte[8 * 1024 * 1024]; // 8MB 缓冲区
    int len;
    while ((len = in.read(buffer)) != -1) {
        process(buffer, 0, len); // 处理当前块
    }
}

内存估算

文件大小推荐缓冲区峰值内存占用
< 100MB8KB< 100MB
100MB ~ 1GB1MB< 200MB
1GB ~ 10GB8MB< 500MB
> 10GB内存映射(分段)可控

方案一:分块读取

适合:需要处理文件内容的每一部分。

java
public static void processLargeFile(String path) throws IOException {
    try (
        BufferedInputStream in = new BufferedInputStream(
            new FileInputStream(path), 8 * 1024 * 1024)
    ) {
        byte[] buffer = new byte[8 * 1024 * 1024]; // 8MB
        int len;
        while ((len = in.read(buffer)) != -1) {
            process(buffer, 0, len);
        }
    }
}

private static void process(byte[] buffer, int offset, int len) {
    // 处理数据块
}

方案二:NIO 内存映射

适合:需要频繁随机访问的大文件。

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);
            // 像操作内存一样操作文件
            while (buffer.hasRemaining()) {
                buffer.put(buffer.get());
            }
            position += size;
        }
    }
}

方案三:零拷贝(只传输)

适合:只需要传输大文件(如文件服务器)。

java
public static void transferLargeFile(String src, String dst) throws IOException {
    try (
        FileChannel in = new FileInputStream(src).getChannel();
        FileChannel out = new FileOutputStream(dst).getChannel()
    ) {
        long size = in.size();
        long transferred = 0;
        while (transferred < size) {
            transferred += in.transferTo(
                transferred,
                size - transferred,
                out
            );
        }
    }
}

带进度的大文件拷贝

java
public static void copyLargeFileWithProgress(String src, String dst) throws IOException {
    File srcFile = new File(src);
    long totalSize = srcFile.length();
    long copied = 0;

    try (
        BufferedInputStream in = new BufferedInputStream(
            new FileInputStream(src), 8 * 1024 * 1024);
        BufferedOutputStream out = new BufferedOutputStream(
            new FileOutputStream(dst), 8 * 1024 * 1024)
    ) {
        byte[] buffer = new byte[8 * 1024 * 1024]; // 8MB
        int len;

        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
            copied += len;

            int percent = (int) (copied * 100 / totalSize);
            System.out.printf("\r拷贝进度: %d%% (%d/%d MB)",
                percent, copied / 1024 / 1024, totalSize / 1024 / 1024);
        }
        System.out.println();
    }
}

流式处理 CSV 大文件

java
public static void processLargeCsv(String path) throws IOException {
    try (
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(
                new FileInputStream(path), StandardCharsets.UTF_8), 1024 * 1024)
    ) {
        String line;
        int count = 0;
        while ((line = reader.readLine()) != null) {
            processRow(line);
            count++;
            if (count % 100000 == 0) {
                System.out.println("已处理 " + count + " 行");
            }
        }
    }
}

private static void processRow(String line) {
    // 处理每一行
}

选型建议

┌─────────────────────────────────────────────────────────────────┐
│  大文件处理选型:                                                 │
│                                                                 │
│  需要处理内容 → 分块读取                                          │
│  需要随机访问 → 内存映射                                          │
│  只需要传输   → 零拷贝                                            │
│  大日志/CSV  → 流式处理                                          │
└─────────────────────────────────────────────────────────────────┘

总结

  • 永远不要一次性读入大文件
  • 分块读取 + 大缓冲区(1MB~8MB)
  • 内存映射适合随机访问大文件
  • 零拷贝适合只需要传输的大文件

基于 VitePress 构建