OutOfMemoryError:尝试读取大文件时的 Java 堆空间
Posted
技术标签:
【中文标题】OutOfMemoryError:尝试读取大文件时的 Java 堆空间【英文标题】:OutOfMemoryError: Java heap space when trying to read large file 【发布时间】:2015-06-21 01:56:21 【问题描述】:我正在尝试读取大文件(大约 516mb),它有 18 行文本。我尝试自己写下代码,但在尝试读取文件时第一行代码出错:
try(BufferedReader br = new BufferedReader(new FileReader("test.txt")))
String line;
while ((line = br.readLine()) != null)
String fileContent = line;
注意:文件存在且大小约为 516mb。 如果有另一种更安全、更快的阅读方法,请告诉我(即使它会换行)。 编辑: 在这里我尝试使用扫描仪,但它持续时间更长,然后给出同样的错误
try(BufferedReader br = new BufferedReader(new FileReader("test.txt")))
Scanner scanner = new Scanner(br);
while(scanner.hasNext())
int index = Integer.parseInt(scanner.next());
// and here do something with index
我什至将文件拆分为 1800 行,但没有得到任何修复
【问题讨论】:
是否需要将整个文件加载到内存中? @higuaro 是的。我想对该文件进行排序 @higuaro 或者有没有办法通过循环单独读取该文件? @user3260312 您有一个包含516M
和18 行的文件要排序?您要对哪种类型的文本进行排序?
@UwePlonus 0-100 之间用空格分隔的随机数。我已经知道该怎么做,但是这个 OutOfMemoryError 毁了我的程序
【参考方案1】:
使用BufferedReader
已经可以帮助您避免将整个文件加载到内存中。所以,为了进一步改进,正如你提到的,每个数字都用空格分隔,所以不要这样:
line = br.readLine();
我们可以用扫描仪包裹阅读器,
Scanner scanner = new Scanner(br);
并使用scanner.next();
提取文件中的每个数字并将其存储到整数数组中也有助于减少内存使用:
int val = Integer.parseInt(scanner.next());
这将帮助您避免阅读整个句子。
您还可以限制BufferedReader
的缓冲区大小
BufferedReader br = new BufferedReader(new FileReader("test.txt") , 8*1024);
更多信息Does the Scanner class load the entire file into memory at once?
【讨论】:
BufferedReader中缓冲区的默认大小已经是8192字节了,手动设置成这个值没有意义。 @user3260312 太慢了?那么你的记忆错误消失了吗?你能再描述一下吗? :) @PhamTrung 它运行了大约 3 分钟,最后中断并显示 OutofmemoryError @user3260312 尝试关注this。因此,不要使用BufferedReader
,而是直接使用FileInputStream
。在文章中,作者正在处理一个 2GB 的文件,所以这应该会有所帮助。
@user3260312 你为堆设置了多少空间?将其设置为至少 256MB :)【参考方案2】:
使用-Xmx
增加堆大小。
对于您的文件,我建议至少设置为-Xmx1536m
,因为加载时文件大小会增加 516M。在内部,Java 使用 16 位来表示一个字符,因此一个 10 字节文本的文件大约需要 10 个字节。 String
为 20 个字节(使用包含许多组合字符的 UTF-8 时除外)。
【讨论】:
它会导致任何问题还是我的程序性能会变慢? @user3260312 只要计算机有足够的主内存,增加内存大小应该没有问题。如果您没有足够的主内存,那么您必须寻找另一种解决方案(独立于您的编程语言)。 虽然没有直接关系——说 Java 内部使用 16 位来表示一个字符并不完全正确。 Java 使用 UTF-16 作为 Unicode 的字符编码;并非所有 Unicode 字符都可以映射到 16 位值,这意味着有些字符需要两个 16 位代码单元。 @m3th0dman 这是不正确的,我知道。但出于实际目的,粗略假设计算基本内存消耗就足够了......而且很少使用代理对......【参考方案3】:Java 旨在处理比可用内存更大的大量数据。在情人级别的 API 文件是一个流,可能是无穷无尽的。
但是,对于芯片内存,人们更喜欢简单的方法 - 将所有内容读入内存并使用内存。通常它有效,但不适用于您的情况。增加内存只会隐藏这个问题,直到你有更大的文件。所以,是时候做对了。
我不知道您的排序方法用于比较。如果它是好的,那么它可能会为每个字符串生成一些可排序的键或索引。您读取文件一次,创建这些键的映射,对它们进行排序,然后基于此排序映射创建排序文件。在您的情况下,这将是(最坏的情况)1+18 个文件读取加上 1 个写入。
但是,如果您没有这样的键并且只是逐个字符地比较字符串,那么您必须有 2 个输入流并相互比较。如果一个字符串不在正确的位置,那么您以正确的顺序重写文件并再次执行。最坏的情况是要比较 18*18 的读数,18*2 的阅读和 18 次的写作。
当您将数据保存在大文件中的大字符串中时,这就是这种架构的结果。
【讨论】:
【参考方案4】:EDIT java堆空间也一样,在循环内或循环外声明变量。
只是一个建议。
如果可以的话,你不应该在循环中声明变量,因为这样,你可以填满java堆空间。在这个例子中,如果可能的话,那就更好了:
try(BufferedReader br = new BufferedReader(new FileReader("test.txt")))
String line;
String fileContent;
while ((line = br.readLine()) != null)
fileContent = line;
为什么?因为在每次迭代中,java 都在堆中为同一个变量保留新空间(Java 正在考虑一个新的不同变量(你可能想要这个,但可能不想要)),如果循环足够大,堆可能会满。
【讨论】:
并非如此,这些变量在 while 循环每完成一个循环时就会被释放,因此 gc 会删除它们。编译器可能已经在优化这个了。 好的,谢谢@RaphMclee,我认为 gc 只有在循环结束时才会删除它们。感谢您提供信息。【参考方案5】:注意: 增加堆内存限制以对 18 行文件进行排序只是解决编程问题的一种懒惰方式,这种总是增加内存而不是解决实际问题的哲学是Java程序因速度慢等而名声不佳的原因。
我的建议是,为了避免增加此类任务的内存,按行拆分文件并以类似于 MergeSort 的方式合并行。这样,如果文件大小增加,您的程序可以扩展。
要将文件拆分为多个“行子文件”,请使用BufferedReader
类的read
方法:
private void splitBigFile() throws IOException
// A 10 Mb buffer size is decent enough
final int BUFFER_SIZE = 1024 * 1024 * 10;
try (BufferedReader br = new BufferedReader(new FileReader("test.txt")))
String line;
int fileIndex = 0;
FileWriter currentSplitFile = new FileWriter(new File("test_split.txt." + fileIndex));
char buffer[] = new char[BUFFER_SIZE];
int readed = 0;
while ((readed = br.read(buffer)) != -1)
// Inspect the buffer in search of the new line character
boolean endLineProcessed = false;
for (int i = 0; i < readed; i++)
if (buffer[i] == '\n')
// This chunk contains the new line character, write this last chunk the current file and create a new one
currentSplitFile.write(buffer, 0, i);
fileIndex++;
currentSplitFile = new FileWriter(new File("test_split.txt." + fileIndex));
currentSplitFile.write(buffer, i, readed - i);
endLineProcessed = true;
// If not end of line found, just write the chunk
if (!endLineProcessed)
currentSplitFile.write(buffer, 0, readed);
要合并它们,打开所有文件并为每个文件保留一个单独的缓冲区(一个小缓冲区,例如每个 2 mb),读取每个文件的第一个块,然后就可以了将有足够的信息开始重新排列文件的索引。如果某些文件有关联,请继续阅读块。
【讨论】:
“...是 Java 程序因缓慢等而名声不佳的原因”-您说的是真的,但不仅限于 Java 程序...不幸的是。 即使这个解决方案也有它的限制,因为文件大小为 516m,只有 18 行是巨大的,所以即使是分割的文件也有合理的大小...... 分割文件不是那么小也没关系,一旦行被分隔,它们可以使用小缓冲区排列,而无需将任何文件完全加载到内存中,并且解决方案可以扩展到更多行。恕我直言,这仍然比增加堆加载整个文件的内存效率更高【参考方案6】:如果不了解应用程序的内存配置文件、JVM 设置和硬件,就很难猜到。它可以像更改 JVM 内存设置一样简单,也可以像使用 RandomFileAccess 并自行转换字节一样困难。我会在这里试一试。问题可能在于您试图读取很长的行,而不是文件很大。
如果您查看 BufferedReader.readLine() 的实现,您会看到类似这样的内容(简化版):
String readLine()
StringBuffer sb = new StringBuffer(defaultStringBufferCapacity);
while (true)
if (endOfLine) return sb.toString();
fillInternalBufferAndAdvancePointers(defaultCharBufferCapacity);//(*)
sb.append(internalBuffer); //(**)
// defaultStringBufferCapacity = 80, can't be changed
// defaultCharBufferCapacity = 8*1024, can be altered
(*) 是这里最关键的一行。它尝试填充有限大小 8K 的内部缓冲区并将字符缓冲区附加到 StringBuffer。 18 行的 516Mb 文件意味着每行将占用约 28Mb 的内存。因此它尝试分配和复制 8K 数组,每行约 3500 次。
(**)然后它会尝试将此数组放入默认容量为 80 的 StringBuffer 中。这会导致 StringBuffer 的额外分配,以确保它的内部缓冲区足够大以保持字符串〜如果我不是每行 25 个额外分配错误。
所以基本上,我建议将内部缓冲区的大小增加到 1Mb,只需将额外的参数传递给 BufferedReader 的实例,例如:
new BufferedReader(..., 1024*1024);
【讨论】:
以上是关于OutOfMemoryError:尝试读取大文件时的 Java 堆空间的主要内容,如果未能解决你的问题,请参考以下文章
读取大小为 330MB 的图像时发生“java.lang.OutOfMemoryError:Java 堆空间”[重复]
17 记一次 spark 读取大数据表 OOM OutOfMemoryError: GC overhead limit exceeded
17 记一次 spark 读取大数据表 OOM OutOfMemoryError: GC overhead limit exceeded