CodeGym /课程 /JAVA 25 SELF /处理大文件:chunking、memory mapping

处理大文件:chunking、memory mapping

JAVA 25 SELF
第 41 级 , 课程 3
可用

1. Chunking — 按块读取文件

正如我们在前一讲中讨论过的,chunking 允许按块处理文件,而不是把整个文件一次性载入内存。这在处理大体量数据时尤为重要。如果文件只有 10 MB,通常没问题——可以直接载入,并用任意方式处理。但如果文件达到 10 GB,而内存只有 8 GB,再加上浏览器开着几十个标签页、IDE 也在运行?尝试整体读取往往会以悲剧收场:OutOfMemoryError、程序卡死以及开发者的泪水。

这类超大文件在现实中很常见:一个月的服务器日志可能有数十 GB,大型 CSV 文件包含数百万行,而视频、压缩包与数据库转储更是体量惊人。

关键思想依然不变:不要“一次吃掉整头大象”,而是分块处理。正是 chunking 让我们能够安全高效地处理此类数据,把文件拆分为可控的部分。

再谈 chunking

Chunk(块)就是文件的一个固定大小的片段。与其一次性读完,我们例如每次读取 4 MB(或 64 KB、或 1 MB——视情况而定)。

原理:

  • 打开文件读取流。
  • 创建缓冲区——固定大小的字节数组。
  • 循环从文件读入缓冲区,直到到达末尾。
  • 每个“块”单独处理。

示例:按块复制大文件

假设我们有一个超大的文件需要复制。让我们写个程序,用“专业”的方式完成它。

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class BigFileCopy {
    public static void main(String[] args) throws IOException {
        String source = "bigfile.dat";
        String dest = "bigfile_copy.dat";
        int bufferSize = 4 * 1024 * 1024; // 4 MB

        try (FileInputStream in = new FileInputStream(source);
             FileOutputStream out = new FileOutputStream(dest)) {

            byte[] buffer = new byte[bufferSize];
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
                // 可以添加进度输出或数据处理
            }
        }
        System.out.println("复制完成!");
    }
}

在 Java 中处理文件通常使用标准流 FileInputStreamFileOutputStream。经验上使用约 4 MB 的缓冲区,对于现代磁盘已足够高效地进行读写。在循环中,程序按块读取文件并立即写入新文件,而不是把整个文件驻留在内存中。

这种方式可以节省内存,避免 OutOfMemoryError 之类的错误,并能处理几乎任意大小的文件,即便是 100 GB 甚至更大的文件。

2. 用 chunking 处理数据

任务往往不是简单复制文件,而是例如查找特定字符串、统计出现次数、替换内容等。

示例:在大型文本文件中搜索字符串

如果是文本文件,使用字符流并按行读取会更方便:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class BigFileSearch {
    public static void main(String[] args) throws IOException {
        String file = "biglog.txt";
        String keyword = "ERROR";
        int count = 0;

        try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
            String line;
            while ((line = reader.readLine()) != null) {
                if (line.contains(keyword)) {
                    count++;
                }
            }
        }
        System.out.println("找到 " + count + " 行包含 ERROR");
    }
}

为什么这对 GB 级文件也有效?

  • BufferedReader 按块读取文件(默认缓冲区为 8 KB,但可以设置更大)。
  • 任一时刻内存中只保留一行。

缓冲区大小:如何选择?

黄金法则:缓冲区太小——磁盘访问过多;太大——浪费内存。

  • 对现代 HDD/SSD,通常 64 KB–4 MB 表现良好。
  • 对网络存储或非常快速的 SSD——可以更大(8–16 MB)。
  • 对文本文件——可以增大 BufferedReader 的缓冲区。

多做实验!用不同的缓冲区大小测量程序运行时间。有时增大缓冲区能带来 2–3 倍的加速,有时几乎没有影响。

3. 内存映射文件(memory-mapped files)

这究竟是什么?

Memory mapping 是一种通过操作系统机制将文件直接映射到进程内存的方式。在 Java 中可使用 MappedByteBuffer(来自 java.nio 包)。文件仿佛变成了一个巨大字节数组,可以直接操作,而无需显式地逐块读写。

这种方法对超大文件特别有用。操作系统会按需把文件的部分加载到内存中,而你可以像操作普通数组一样访问文件的任意位置。Memory-mapped files 提供了高性能的随机访问。例如,当需要快速地从文件的不同位置读取片段而无需整体加载时。

代码示例

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class MemoryMappedRead {
    public static void main(String[] args) throws Exception {
        String fileName = "bigfile.dat";
        try (RandomAccessFile file = new RandomAccessFile(fileName, "r");
             FileChannel channel = file.getChannel()) {

            long fileSize = channel.size();
            int chunkSize = 1024 * 1024 * 128; // 128 MB — 单个映射的大小

            long position = 0;
            while (position < fileSize) {
                long size = Math.min(chunkSize, fileSize - position);
                MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, position, size);

                // 像从数组一样从 buffer 读取数据
                for (int i = 0; i < size; i++) {
                    byte b = buffer.get(i);
                    // 处理字节(例如查找特定值)
                }

                position += size;
            }
        }
        System.out.println("通过 memory mapping 读取完成!");
    }
}

RandomAccessFileFileChannel 允许以较低层级访问文件。调用 channel.map 会把文件的一个片段映射到内存。数据访问通过 MappedByteBuffer 缓冲区完成。

memory mapping 的优点是什么?

  • 对文件不同位置的随机访问非常快。
  • 可以处理大于可用物理内存的文件(操作系统按需加载页面)。
  • 被广泛用于现代数据库、索引、海量日志等场景。

缺点是什么?

  • 并不总适合写入(尤其是网络文件系统)。
  • 映射大小有限制(在 32 位 JVM 中单次映射通常至多 2 GB)。
  • 如果忘记关闭文件,可能导致文件被占用而无法释放(尤其在 Windows)。
  • 并非所有操作都会加速——若只是顺序读取,普通缓冲往往并不逊色。

4. 实用示例

示例 1:通过 memory mapping 在大文件中查找子串

假设有一个 10 GB 的文件,需要在其中查找某个字节序列(例如字符串 "SECRET")。

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;

public class MemoryMappedSearch {
    public static void main(String[] args) throws Exception {
        String fileName = "hugefile.bin";
        byte[] target = "SECRET".getBytes(StandardCharsets.UTF_8);

        try (RandomAccessFile file = new RandomAccessFile(fileName, "r");
             FileChannel channel = file.getChannel()) {

            long fileSize = channel.size();
            int chunkSize = 128 * 1024 * 1024; // 128 MB

            long position = 0;
            while (position < fileSize) {
                long size = Math.min(chunkSize, fileSize - position);
                MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, position, size);

                for (int i = 0; i < size - target.length; i++) {
                    boolean found = true;
                    for (int j = 0; j < target.length; j++) {
                        if (buffer.get(i + j) != target[j]) {
                            found = false;
                            break;
                        }
                    }
                    if (found) {
                        System.out.println("在位置 " + (position + i) + " 处找到");
                        // 可以停止或继续搜索
                    }
                }
                position += size;
            }
        }
    }
}

注意:
如果子串可能跨越两个块,需要在块之间按目标序列的长度进行重叠。

5. 有用的细节

何时使用 chunking,何时使用 memory mapping?

  • Chunking——适用于任何文件类型(文本、二进制、日志、压缩包),非常适合顺序处理。
  • Memory mapping——在随机访问、大型索引、数据库、对超大文件的快速搜索等场景下非常高效。

如果不确定该选哪一个——先从 chunking 开始!Memory mapping 功力强大,但更“底层”,需要小心使用。

建议

  • 使用 try-with-resources 自动关闭流与通道。
  • 不要同时打开太多文件:操作系统对打开的文件描述符数量有上限。
  • 避免映射过大的片段——可能导致错误(尤其在 32 位 JVM)。
  • 并行处理时可以把文件拆分为多个块,在不同线程中处理(但注意不要把磁盘“打爆”,也不要超出内存限制)。

6. 处理大文件的常见错误

错误 №1:尝试把整个大文件加载到内存。
非常常见——尤其是新手。如果文件大于 1–2 GB,请使用 chunking 或按行读取,否则程序会因 OutOfMemoryError 崩溃。

错误 №2:缓冲区太小。
512 字节的缓冲并不是优化,而是对性能的慢性自杀。请使用至少 64 KB 以上的缓冲区。

错误 №3:忘记关闭流或通道。
文件描述符会一直占用,文件无法删除或释放,直到重启 JVM。请使用 try-with-resources

错误 №4:不正确地使用 memory mapping。
如果文件在映射期间被其他进程修改,可能得到不一致的数据或出现错误。不要对频繁变化的文件使用 memory mapping。

错误 №5:搜索子串时未考虑块重叠。
若目标字符串可能出现在两个块的“接缝”处,务必在块之间按照该字符串的长度进行重叠。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION