尝试读取大文件时出现OutOfMemoryError: Java堆空间错误

8

我正在尝试读取一个大文件(约516MB),其中有18行文本。我尝试亲自编写代码时,在试图读取该文件的第一行代码上出现了错误:

 try(BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
        String line;
        while ((line = br.readLine()) != null) {
            String fileContent = line;
        }
 }

注意:文件已存在,大约大小为516mb。 如果有其他更安全、更快速的读取方法,请告诉我(即使它会换行)。 编辑: 我试过使用Scanner,但它需要更长时间,然后会给出相同的错误。

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行,但仍然没有解决问题。


1
你需要将整个文件加载到内存中吗? - higuaro
@higuaro 是的,我想要对那个文件进行排序。 - user3260312
实际上,对于这种类型的数据,您可以将文件分成几个较小的文件,并逐个处理每个较小的文件,只需使用一个数组data [101]来计算频率,您有足够的空间。 - Pham Trung
@higuaro 我以为你会写一些答案... - user3260312
@PhamTrung 是的,我已经写了那段计算频率的代码,但由于一个错误,我无法继续)) - user3260312
显示剩余7条评论
6个回答

4

使用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);

更多信息请参考Scanner类是否一次性将整个文件加载到内存中?


@user3260312 请尝试遵循这个链接。因此,您可以直接使用FileInputStream而不是BufferedReader。在文章中,作者正在处理一个2GB的文件,所以这应该会有所帮助。 - Pham Trung
@user3260312 你为堆设置了多少空间?请将其至少设置为256MB :) - Pham Trung
@user3260312 哈哈,没关系,那只是你的内存RAM,你的程序会运行得更快 :) - Pham Trung
@PhamTrung 我应该像Uwe说的那样将其设置为-Xmx1536m吗? - user3260312
@user3260312 你可以自己试一下,我建议你使用二分查找来找出对你的程序最好的解决方案 :) - Pham Trung
显示剩余5条评论

1

编辑 对于Java堆空间而言,无论是在循环内部还是外部声明变量都一样。

这只是一个建议。

如果可以的话,你不应该在循环内部声明变量,因为这样会填满Java堆空间。在这个例子中,如果可能的话,最好这样做:

try(BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
        String line;
        String fileContent;
        while ((line = br.readLine()) != null) {
            fileContent = line;
        }
 } 

因为在每次迭代中,Java 都会为同一变量在堆中保留新空间(Java 认为这是一个新的不同变量(你可能想要这个,但很可能不需要)),如果循环足够大,堆可能会被填满。

您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - RaphMclee
好的,谢谢@RaphMclee。我以为垃圾回收只会在循环结束后才移除它们。感谢您提供的信息。 - maiklahoz

1
使用-Xmx增加堆大小。
对于您的文件,我建议至少设置-Xmx1536m,因为文件大小在加载时会增加到516M。在内部,Java使用16位表示一个字符,因此一个包含10个字节文本的文件将占用大约20个字节作为String(除非使用具有许多组合字符的UTF-8)。

这会引起任何问题吗?或者会降低我的程序性能吗? - user3260312
只要计算机具有足够的主存储器,增加内存大小就不应该有问题。如果您没有足够的主存储器,则必须寻找另一种解决方案(与您的编程语言无关)。 - Uwe Plonus
虽然并非直接相关,但说Java内部使用16位来表示字符并不完全正确。Java使用UTF-16作为Unicode的字符编码方式;而且,并非所有的Unicode字符都可以映射到16位值,这意味着有些字符需要两个16位代码单元来表示。 - Random42
@m3th0dman 这并不正确,我知道。但是为了实际目的,它足以作为计算基本内存消耗的粗略估计...此外代理对很少被使用... - Uwe Plonus

1

Java被设计用于处理超过可用内存的大量数据。在较低级别的API中,文件是一个流,可能是无限的。

但是,随着芯片内存的增加,人们更喜欢简单的方法 - 将所有内容都读入内存并使用内存进行操作。通常这样做是有效的,但不适用于你的情况。增加内存只能隐藏这个问题,直到你有更大的文件。所以,现在是时候正确地解决它了。

我不知道你用来比较的排序方法是什么。如果是好的排序方法,那么它可能会生成每个字符串的可排序键或索引。你只需读取文件一次,创建此类键的映射,对其进行排序,然后基于此排序映射创建排序后的文件。这将是(在最坏的情况下)1+18个文件读取加上1个写入。

但是,如果你没有这样的键,只是按字符比较字符串,那么你必须有两个输入流并将一个流与另一个流进行比较。如果一个字符串没有在正确的位置上,则需要按正确的顺序重写文件,然后再次进行比较。在最坏的情况下,需要进行18*18次读取才能进行比较,进行18*2次读取以进行写入,并进行18次写入。

这就是当你将数据保存在巨大的文件和字符串中时,这种架构的后果。


0

注意:将堆内存限制增加以对18行文件进行排序只是解决编程问题的懒惰方式,这种总是增加内存而不解决实际问题的哲学是Java程序缓慢等不良声誉的原因之一。

我的建议是,为避免为此任务增加内存,可以按行拆分文件,并以类似于归并排序的方式合并行。这样,如果文件大小增长,您的程序就可以扩展。

要将文件拆分为多个“行子文件”,请使用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),读取每个文件的第一块,这样您就有足够的信息开始重新排列文件的索引。如果某些文件存在关联,则继续读取块。


2
“...是Java程序声名不佳的缓慢等问题的原因。” - 你说的没错,但这不仅仅局限于Java程序...不幸的是。 - Mr Moose
即使这个解决方案也有其局限性,因为一个大小为516m且只有18行的文件是巨大的,所以即使分割后的文件也具有合理的大小... - Uwe Plonus
无论拆分的文件是否很小,一旦行被分隔开来,就可以使用小缓冲区排列它们,而不需要完全将任何文件加载到内存中。这种解决方案适用于更多行,并且在我看来,这仍然比增加堆以加载整个文件更具有内存效率。 - higuaro

0
没有了解您的应用程序的内存配置文件、JVM设置和硬件,很难猜测。解决这个问题可能只需要改变JVM内存设置,也可能需要使用RandomFileAccess并自行转换字节。我会尝试一个长 Shot。问题可能只是因为您试图读取非常长的行,而不是因为文件很大。
如果您查看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);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接