高效读取大文件,再也不用担心 OOM 了!
内存读取
第一个版本,采用内存读取的方式,所有的数据首先读读取到内存中,程序代码如下:
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 IO
Java8 stream
BufferReader
我们可以使用 BufferReader#readLine 逐行读取数据。
try (BufferedReader fileBufferReader = new BufferedReader(new FileReader("temp/test.txt"))) {
String fileLineContent;
while ((fileLineContent = fileBufferReader.readLine()) != null) {
// process the line.
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
Apache Commons IO
Common-IO 中有一个方法 FileUtils#lineIterator可以实现逐行读取方式,使用代码如下:
Stopwatch stopwatch = Stopwatch.createStarted();
LineIterator fileContents = FileUtils.lineIterator(new File("temp/test.txt"), StandardCharsets.UTF_8.name());
while (fileContents.hasNext()) {
fileContents.nextLine();
// pass
}
logMemory();
fileContents.close();
stopwatch.stop();
System.out.println("read all lines spend " + stopwatch.elapsed(TimeUnit.SECONDS) + " s");
这个方法返回一个迭代器,每次我们都可以获取的一行数据。
其实我们查看代码,其实可以发现 FileUtils#lineIterator,其实用的就是 BufferReader,感兴趣的同学可以自己查看一下源码。
Java8 stream
Java8 Files 类新增了一个 lines,可以返回 Stream我们可以逐行处理数据。
Stopwatch stopwatch = Stopwatch.createStarted();
// lines(Path path, Charset cs)
try (Stream<String> inputStream = Files.lines(Paths.get("temp/test.txt"), StandardCharsets.UTF_8)) {
inputStream
.filter(str -> str.length() > 5)// 过滤数据
.forEach(o -> {
// pass do sample logic
});
}
logMemory();
stopwatch.stop();
System.out.println("read all lines spend " + stopwatch.elapsed(TimeUnit.SECONDS) + " s");
使用这个方法有个好处在于,我们可以方便使用 Stream 链式操作,做一些过滤操作。
注意:这里我们使用
try-with-resources方式,可以安全的确保读取结束,流可以被安全的关闭。
并发读取
逐行的读取的方式,解决我们 OOM 的问题。不过如果数据很多,我们这样一行行处理,需要花费很多时间。
上述的方式,只有一个线程在处理数据,那其实我们可以多来几个线程,增加并行度。
下面在上面的基础上,就抛砖引玉,介绍下自己比较常用两种并行处理方式。
逐行批次打包
第一种方式,先逐行读取数据,加载到内存中,等到积累一定数据之后,然后再交给线程池异步处理。
@SneakyThrows
public static void readInApacheIOWithThreadPool() {
// 创建一个 最大线程数为 10,队列最大数为 100 的线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 60l, TimeUnit.SECONDS, new LinkedBlockingDeque<>(100));
// 使用 Apache 的方式逐行读取数据
LineIterator fileContents = FileUtils.lineIterator(new File("temp/test.txt"), StandardCharsets.UTF_8.name());
List<String> lines = Lists.newArrayList();
while (fileContents.hasNext()) {
String nextLine = fileContents.nextLine();
lines.add(nextLine);
// 读取到十万的时候
if (lines.size() == 100000) {
// 拆分成两个 50000 ,交给异步线程处理
List<List<String>> partition = Lists.partition(lines, 50000);
List<Future> futureList = Lists.newArrayList();
for (List<String> strings : partition) {
Future<?> future = threadPoolExecutor.submit(() -> {
processTask(strings);
});
futureList.add(future);
}
// 等待两个线程将任务执行结束之后,再次读取数据。这样的目的防止,任务过多,加载的数据过多,导致 OOM
for (Future future : futureList) {
// 等待执行结束
future.get();
}
// 清除内容
lines.clear();
} }
// lines 若还有剩余,继续执行结束
if (!lines.isEmpty()) {
// 继续执行
processTask(lines);
}
threadPoolExecutor.shutdown();
}
private static void processTask(List<String> strings) {
for (String line : strings) {
// 模拟业务执行
try {
TimeUnit.MILLISECONDS.sleep(10L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上述方法,等到内存的数据到达 10000 的时候,拆封两个任务交给异步线程执行,每个任务分别处理 50000 行数据。
后续使用 future#get(),等待异步线程执行完成之后,主线程才能继续读取数据。
之所以这么做,主要原因是因为,线程池的任务过多,再次导致 OOM 的问题。
大文件拆分成小文件
第二种方式,首先我们将一个大文件拆分成几个小文件,然后使用多个异步线程分别逐行处理数据。
public static void splitFileAndRead() throws Exception {
// 先将大文件拆分成小文件
List<File> fileList = splitLargeFile("temp/test.txt");
// 创建一个 最大线程数为 10,队列最大数为 100 的线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 60l, TimeUnit.SECONDS, new LinkedBlockingDeque<>(100));
List<Future> futureList = Lists.newArrayList();
for (File file : fileList) {
Future<?> future = threadPoolExecutor.submit(() -> {
try (Stream inputStream = Files.lines(file.toPath(), StandardCharsets.UTF_8)) {
inputStream.forEach(o -> {
// 模拟执行业务
try {
TimeUnit.MILLISECONDS.sleep(10L);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
} catch (IOException e) {
e.printStackTrace();
}
});
futureList.add(future);
}
for (Future future : futureList) {
// 等待所有任务执行结束
future.get();
}
threadPoolExecutor.shutdown();
}
private static List<File> splitLargeFile(String largeFileName) throws IOException {
LineIterator fileContents = FileUtils.lineIterator(new File(largeFileName), StandardCharsets.UTF_8.name());
List<String> lines = Lists.newArrayList();
// 文件序号
int num = 1;
List<File> files = Lists.newArrayList();
while (fileContents.hasNext()) {
String nextLine = fileContents.nextLine();
lines.add(nextLine);
// 每个文件 10w 行数据
if (lines.size() == 100000) {
createSmallFile(lines, num, files);
num++;
}
}
// lines 若还有剩余,继续执行结束
if (!lines.isEmpty()) {
// 继续执行
createSmallFile(lines, num, files);
}
return files;
}
上述方法,首先将一个大文件拆分成多个保存 10W 行的数据的小文件,然后再将小文件交给线程池异步处理。
由于这里的异步线程每次都是逐行从小文件的读取数据,所以这种方式不用像上面方法一样担心 OOM 的问题。
另外,上述我们使用 Java 代码,将大文件拆分成小文件。这里还有一个简单的办法,我们可以直接使用下述命令,直接将大文件拆分成小文件:
# 将大文件拆分成 100000 的小文件
split -l 100000 test.txt
后续 Java 代码只需要直接读取小文件即可。
总结
当我们从文件读取数据时,如果文件不是很大,我们可以考虑一次性读取到内存中,然后快速处理。
如果文件过大,我们就没办法一次性加载到内存中,所以我们需要考虑逐行读取,然后处理数据。但是单线程处理数据毕竟有限,所以我们考虑使用多线程,加快处理数据。
本篇文章我们只是简单介绍了下,数据从文件读取几种方式。数据读取之后,我们肯定还需要处理,然后最后会存储到数据库中或者输出到另一个文件中。
这个过程,说实话比较麻烦,因为我们的数据源文件,可能是 txt,也可能是 excel,这样我们就需要增加多种读取方法。同样的,当数据处理完成之后,也有同样的问题。
不过好在,上述的问题我们可以使用 Spring Batch 完美解决。
高效读取大文件,再也不用担心 OOM 了!的更多相关文章
- Java高效读取大文件
1.概述 本教程将演示如何用Java高效地读取大文件.这篇文章是Baeldung (http://www.baeldung.com/) 上“Java——回归基础”系列教程的一部分. 2.在内存中读取 ...
- Java高效读取大文件(转)
1.概述 本教程将演示如何用Java高效地读取大文件.这篇文章是Baeldung(http://www.baeldung.com/) 上“Java——回归基础”系列教程的一部分. 2.在内存中读取 读 ...
- 完全免费,再也不用担心转pdf文件乱来乱去的问题了
完全免费,再也不用担心转pdf文件乱来乱去的问题了. 源代码:https://github.com/xlgwr/WpsToPdf.git 第三方插件Bye Bye... 功能说明 主要引用Wps金山办 ...
- 使用BeautifulSoup高效解析网页,再也不用担心睡不着觉了
BeautifulSoup是一个可以从 HTML 或 XML 文件中提取数据的 Python 库 那需要怎么使用呢? 首先我们要安装一下这个库 1.pip install beautifulsoup4 ...
- php如何高效的读取大文件
通常来说在php读取大文件的时候,我们采用的方法一般是一行行来讲取,而不是一次性把文件全部写入内存中,这样会导致php程序卡死,下面就给大家介绍这样一个例子. 需求:有一个800M的日志文件,大约有5 ...
- 妈妈再也不用担心别人问我是否真正用过redis了
1. Memcache与Redis的区别 1.1. 存储方式不同 1.2. 数据支持类型 1.3. 使用底层模型不同 2. Redis支持的数据类型 3. Redis的回收策略 4. Redis小命令 ...
- 教会舍友玩 Git (再也不用担心他的学习)
舍友长大想当程序员,我和他爷爷奶奶都可高兴了,写他最喜欢的喜之郎牌Git文章,学完以后,再也不用担心舍友的学习了(狗头)哪里不会写哪里 ~~~ 一 先来聊一聊 太多东西属于,总在用,但是一直都没整理的 ...
- 保姆级神器 Maven,再也不用担心项目构建搞崩了
今天来给大家介绍一款项目构建神器--Maven,不仅能帮我们自动化构建,还能够抽象构建过程,提供构建任务实现:它跨平台,对外提供了一致的操作接口,这一切足以使它成为优秀的.流行的构建工具,从此以后,再 ...
- 【阿里云产品公测】离线归档OAS,再也不用担心备份空间了
[阿里云产品公测]离线归档OAS,再也不用担心备份空间了 作者:阿里云用户莫须有3i 1 起步 1.1 初识OAS 啥是OAS,请看官方说明: 引用: 开放归档服务(Open Archive Se ...
随机推荐
- js this指向汇总
this指向 普通函数 window 定时器函数 window 事件函数 事件源 箭头函数 父function中的this,没有就是window 对象函数 对象本身 构造函数 实例化 ...
- Centos 7 端口聚合
简单粗暴,直接复制命令就好了 还是先啰嗦一下,添加网卡之后,如果没有网卡配置文件,可以通过nmcli con show 先查看网卡的唯一ID,然后复制其他的网卡配置文件,修改device项,name项 ...
- 使用Visual Studio 2019将ASP.NET Core发布为linux-arm64程序
前言 前段时间入手了一台树莓派4B,一直闲置未使用,最近工作需要,要在上面跑下.NET Core程序,由于树莓派4B使用的是ARM架构,并且支持64位操作系统,为了充分发挥树莓派性能,我的这台树莓派安 ...
- Jenkins 邮件发送
1.jenkins新建任务 2.配置svn 3.maven项目构建配置pom.xml 使用maven命令 clean test 构建前清除: 4.系统管理 => 插件管理 =>可选安装邮件 ...
- idea Mark Directory as 的几种文件类型
1. Source roots (or source folders) 源文件夹 通过为该类别分配文件夹,可以告诉IntelliJ IDEA该文件夹及其子文件夹包含应在构建过程中进行编译的源代码. 2 ...
- MySQL:由于找不到VCRUNTIME140_1.dll,无法继续执行代码。重新安装程序可能会解决此问题
我只是搬用工,记录一下 方法一: 安装这个微软常用运行库合集(https://www.repaik.com/), 链接:https://pan.baidu.com/s/1r4JJaUKjw-y1g3l ...
- uni-app map组件关于marker标记点动态设置的问题
marker是Array类型,赋值的时候只能对整个数组进行更改赋值,不能只改变内部的对象,亲测Vue.$set()也不行 this.marker = [ { latitude: 39.90, long ...
- js判断是否是同一域名
可以判断自己的网页是否是嵌入别的网页中 /** * 是否相同域名 * @returns {boolean} * @constructor */ function SameDomain() { try ...
- redis异常:(error) MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk.
(error) MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on d ...
- 如何修改visual-studio的sln文件和project工程名
关于VS的 .sln 文件和 .suo 文件 *.sln:(Visual Studio.Solution) 通过为环境提供对项目.项目项和解决方案项在磁盘上位置的引用,可将它们组织到解决方案中.比如是 ...