OutOfMemoryError:尝试读取大文件时的Java堆空间

时间:2015-04-15 07:53:45

标签: java string algorithm file

我试图读取大文件(大约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行,但没有修复

6 个答案:

答案 0 :(得分: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);

更多信息Does the Scanner class load the entire file into memory at once?

答案 1 :(得分:1)

增加-Xmx的堆大小。

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

答案 2 :(得分:1)

Java旨在处理大量可用内存的大量数据。在情人级API文件是一个流,可能是无穷无尽的。

然而,对于芯片内存,人们更喜欢简单的方法 - 全部读取内存并使用内存。通常它可以工作但不是你的情况。增加内存只会隐藏此问题,直到您拥有更大的文件。所以,现在是时候做好了。

我不知道您的排序方法用于比较。如果它是好的那么它可以产生每个字符串的一些可排序的键或索引。您只读取文件一次,创建映射pf这样的键,对它们进行排序,然后根据此有序映射创建排序文件。在你的情况下,这将是(最糟糕的情况)1 + 18文件读数加1写。

但是,如果您没有这样的密钥并且只是逐个字符地比较字符串,那么您必须有2个输入流并相互比较。如果一个字符串不正确,那么您按正确的顺序重写文件并再次执行。最糟糕的案例18 * 18读数比较,18 * 2读写和18写。

当您将数据保存在巨大的文件中时,这就是这种架构的结果。

答案 3 :(得分:0)

注意:增加堆内存限制以对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),读取每个文件的第一个块,然后在那里&# 39; ll有足够的信息来开始重新排列文件的索引。如果某些文件有联系,请继续阅读块。

答案 4 :(得分:0)

如果不了解应用程序的内存配置文件,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的内部缓冲区,并将char缓冲区附加到StringBuffer。带有18行的516Mb文件意味着每行将在内存中占用~28Mb。因此它尝试每行分配和复制8K数组~3500次。

(**)然后它尝试将此数组放入默认容量为80的StringBuffer中。这会导致StringBuffer的额外分配,以确保它的内部缓冲区足够大,以便每行保留字符串~25个额外分配我没弄错。

基本上,我建议将内部缓冲区的大小增加到1Mb,只需将额外的参数传递给BufferedReader的实例,如:

 new BufferedReader(..., 1024*1024);

答案 5 :(得分:0)

编辑对于Java堆空间是相同的,在循环内部或外部声明变量。

只是一个建议。

如果可以的话,你不应该在循环中声明变量,因此,你可以填满java堆空间。在这个例子中,如果可能,那就更好了:

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

为什么呢?因为在每次迭代中,java都在堆中为同一个变量保留新的空间(Java正在考虑一个新的不同变量(你可能想要这个,但可能不需要))如果循环足够大,那么堆就可以满了。