高效读取大文件,再也不用担心 OOM 了!

本文讲述了在处理大文件时遇到的OOM问题,以及如何通过逐行读取(BufferedReader、Apache Commons IO、Java8 Stream)、并发读取(逐行批次打包、大文件拆分)等方式避免内存溢出,提高效率。通过实例代码展示了解决方案,并提到Spring Batch在批量处理数据中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

每天早上七点三十,准时推送干货

Photo by Luisa Brimble on Unsplash

最近阿粉接到一个需求,需要从文件读取数据,然后经过业务处理之后存储到数据库中。这个需求,说实话不是很难,阿粉很快完成了第一个版本。

内存读取

第一个版本,阿粉采用内存读取的方式,所有的数据首先读读取到内存中,程序代码如下:

Stopwatch stopwatch = Stopwatch.createStarted();
// 将全部行数读取的内存中
List<String> lines = FileUtils.readLines(new File("temp/test.txt"), Charset.defaultCharset());
for (String line : lines) {
    // pass
}
stopwatch.stop();
System.out.println("read all lines spend " + stopwatch.elapsed(TimeUnit.SECONDS) + " s");
// 计算内存占用
logMemory();

logMemory方法如下:

MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
//堆内存使用情况
MemoryUsage memoryUsage = memoryMXBean.getHeapMemoryUsage();
//初始的总内存
long totalMemorySize = memoryUsage.getInit();
//已使用的内存
long usedMemorySize = memoryUsage.getUsed();


System.out.println("Total Memory: " + totalMemorySize / (1024 * 1024) + " Mb");
System.out.println("Free Memory: " + usedMemorySize / (1024 * 1024) + " Mb");

上述程序中,阿粉使用 Apache Common-Io 开源第三方库,FileUtils#readLines将会把文件中所有内容,全部读取到内存中。

这个程序简单测试并没有什么问题,但是等拿到真正的数据文件,运行程序,很快程序发生了 OOM

之所以会发生 OOM,主要原因是因为这个数据文件太大。假设上面测试文件 test.txt总共有 200W 行数据,文件大小为:740MB。

通过上述程序读取到内存之后,在我的电脑上内存占用情况如下:

可以看到一个实际大小为 700 多 M 的文件,读到内存中占用内存量为 1.5G 之多。而我之前的程序,虚拟机设置内存大小只有 1G,所以程序发生了 OOM。

当然这里最简单的办法就是加内存呗,将虚拟机内存设置到 2G,甚至更多。不过机器内存始终有限,如果文件更大,还是没有办法全部都加载到内存。

不过仔细一想真的需要将全部数据一次性加载到内存中?

很显然,不需要!

在上述的场景中,我们将数据到加载内存中,最后不还是一条条处理数据。

所以下面我们将读取方式修改成逐行读取。

逐行读取

逐行读取的方式比较多,这里阿粉主要介绍两种方式:

  • BufferReader

  • Apache Commons

<think>嗯,用户问的是如何避免读取或写入超大文件时出现OOM(内存溢出)。这个问题看起来挺常见的,尤其是处理大文件的时候。首先,我得回忆一下常见的处理方法,比如一次性读取整个文件到内存确实容易导致OOM,特别是当文件大小超过可用内存时。 那用户可能是在处理日志文件、CSV数据或者视频、图片等大文件时遇到了问题。他们可能尝试过用普通的读取方法,比如用Java的FileInputStream全部读入byte数组,结果内存爆了。这时候需要找到替代方案,避免一次性加载整个文件。 首先,应该考虑流式处理,逐块读取文件,而不是一次性读取。例如,使用BufferedReader逐行读取,或者用BufferedInputStream分块读取。这样每次只处理一小部分数据,内存占用就会小很多。但用户可能已经知道这些基础方法,需要更详细的优化策略。 然后,可能需要提到内存映射文件,比如Java的FileChannel和MappedByteBuffer。这种方法可以利用操作系统的虚拟内存,将文件映射到内存中,按需加载,减少物理内存占用。这对于随机访问大文件很有用,但用户需要注意处理内存映射的范围,避免映射整个文件。 另外,处理大文件时,要注意正确关闭资源,防止文件句柄泄漏,导致资源耗尽。使用try-with-resources语句可以自动管理资源,避免忘记关闭流。 还有可能涉及到数据处理过程中的中间集合。比如,如果在处理每一行数据时,将结果暂存在一个List中,当数据量很大时,这个List也会占用大量内存。这时候可以考虑边处理边写入,或者使用临时文件存储中间结果,减少内存中的对象数量。 用户可能还会用到第三方库,比如Apache Commons IOIOUtils.copy方法,或者使用NIO的Files.copy来处理大文件,这些方法内部已经做了优化,适合处理大文件传输。 另外,调整JVM的内存参数可能也是一种方法,比如增加堆内存大小,但这只是临时解决方案,并不能根本解决问题,特别是当文件远大于可用内存时,还是会OOM。所以重点应该放在优化读取方式上。 还有可能涉及到编码问题,比如读取文本文件时,字符编码转换可能会占用更多内存,需要注意缓冲区的大小设置,避免过大的缓冲区。 对于写入大文件,同样需要分批次写入,避免在内存中构建整个数据再一次性写入。例如,使用缓冲输出流,定期flush,但不要将所有数据都保留在内存中。 可能还需要提到使用临时文件或分片处理,将大文件分割成小块处理,处理完一块释放一块的内存,这样可以有效控制内存使用。 用户可能还不太清楚如何具体实现这些方法,需要给出代码示例或步骤,比如使用BufferedReader逐行读取,或者使用NIO的FileChannel进行分块读取和写入。 另外,要注意异常处理,确保在读取或写入过程中出现异常时,资源能够正确释放,避免内存泄漏或文件损坏。 总结一下,避免OOM的关键是减少一次性加载的数据量,采用流式处理、分块读写、使用内存映射文件,合理管理中间数据,以及正确配置和优化资源使用。需要根据具体的应用场景选择合适的策略,并注意代码中的资源管理和异常处理。</think>在读取或写入超大文件时,避免OOM(内存溢出)的核心思路是**减少内存驻留数据量**,采用流式处理或分块处理策略。以下是具体方法及原理: --- ### **一、基础原则** 1. **避免全量加载** - **错误做法**:`Files.readAllBytes()`或`FileInputStream.read()`一次性读取整个文件到内存。 - **正确方向**:使用**流式处理(Streaming)**或**分块处理(Chunking)**。 --- ### **二、具体解决方案** #### **1. 读取大文件** - **文本文件(逐行读取)** 使用`BufferedReader`逐行读取,每行处理完立即释放内存: ```java try (BufferedReader reader = new BufferedReader(new FileReader("large_file.txt"))) { String line; while ((line = reader.readLine()) != null) { processLine(line); // 逐行处理 } } ``` - **二进制文件(分块读取)** 使用`BufferedInputStream`分块读取(如每次读取8KB): ```java byte[] buffer = new byte[8192]; // 8KB缓冲区 try (InputStream in = new BufferedInputStream(new FileInputStream("large_file.bin"))) { int bytesRead; while ((bytesRead = in.read(buffer)) != -1) { processChunk(buffer, bytesRead); // 处理当前块 } } ``` - **内存映射文件(Memory-Mapped Files)** 通过`FileChannel`将文件映射到虚拟内存,按需加载(适合随机访问): ```java try (RandomAccessFile file = new RandomAccessFile("large_file.bin", "r"); FileChannel channel = file.getChannel()) { MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()); while (buffer.hasRemaining()) { byte data = buffer.get(); // 按需读取,不占用物理内存 } } ``` --- #### **2. 写入超大文件** - **分批次写入** 避免在内存中构建完整数据集,直接流式写入: ```java try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) { for (Data data : dataSource) { writer.write(data.toString()); // 边生成边写入 } } ``` - **使用临时文件** 若需中间处理,将数据写入临时文件而非内存: ```java Path tempFile = Files.createTempFile("temp", ".dat"); try (OutputStream out = Files.newOutputStream(tempFile)) { for (Data chunk : largeData) { byte[] bytes = serialize(chunk); out.write(bytes); // 分批写入临时文件 } } ``` --- #### **3. 特殊场景优化** - **大文件传输(如网络或磁盘间复制)** 使用零拷贝技术(Zero-Copy)减少内存复制: ```java try (FileChannel source = new FileInputStream("source.bin").getChannel(); FileChannel dest = new FileOutputStream("dest.bin").getChannel()) { source.transferTo(0, source.size(), dest); // 直接传输,不经过用户内存 } ``` - **压缩/解压处理** 流式压缩(如GZIP)避免全量加载: ```java try (GZIPOutputStream gzipOut = new GZIPOutputStream(new FileOutputStream("output.gz"))) { byte[] buffer = new byte[8192]; while (/* 数据未读完 */) { int bytesRead = readChunk(buffer); // 读取数据块 gzipOut.write(buffer, 0, bytesRead); // 流式压缩写入 } } ``` --- ### **三、关键注意事项** 1. **资源释放** - 使用`try-with-resources`确保流、通道等资源自动关闭。 - 手动调用`close()`或`flush()`(如写入关键数据后)。 2. **缓冲区大小调优** - 根据文件大小和硬件性能调整缓冲区(如8KB~1MB)。 - 避免过大的缓冲区导致内存碎片。 3. **避免中间集合** - 如处理CSV时,不要将全部行存入`List<String>`,应逐行解析。 4. **JVM参数调整(辅助手段)** - 适当增加堆内存(`-Xmx`),但需结合物理内存限制。 - 启用G1垃圾回收器减少停顿(`-XX:+UseG1GC`)。 --- ### **四、典型错误案例** - **错误示例**:将10GB文件读入`List<String>` ```java List<String> lines = Files.readAllLines(Paths.get("10GB.txt")); // 直接OOM ``` - **修正方案**:改用流式处理或分页读取。 --- ### **五、工具与库推荐** 1. **Apache Commons IO** `IOUtils.copyLarge()`支持大文件流式复制。 2. **OpenCSV** 流式解析CSV文件,避免全量加载。 3. **Java NIO.2** `Files.lines()`返回惰性加载的流(Java 8+)。 --- 通过上述方法,可在处理TB级文件时仍保持内存占用稳定(通常<100MB),从根本上避免OOM
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值