读取大型文本文件时出现Java内存溢出错误

22

我是Java的新手,正在阅读非常大的文件,并需要帮助理解和解决问题。我们有一些必须进行优化以使其正常运行的旧代码。文件大小仅限于10mb到10gb。只有当文件大小超过800mb时才会出现麻烦。

InputStream inFileReader = channelSFtp.get(path); // file reading from ssh.
byte[] localbuffer = new byte[2048];
ByteArrayOutputStream bArrStream = new ByteArrayOutputStream();

int i = 0;
while (-1 != (i = inFileReader.read(buffer))) {
bArrStream.write(localbuffer, 0, i);
}

byte[] data = bArrStream.toByteArray();
inFileReader.close();
bos.close();

我们遇到了错误。

java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:2271)
    at java.io.ByteArrayOutputStream.grow(ByteArrayOutputStream.java:113)
    at java.io.ByteArrayOutputStream.ensureCapacity(ByteArrayOutputStream.java:93)
    at java.io.ByteArrayOutputStream.write(ByteArrayOutputStream.java:140)

任何帮助都将不胜感激?


1
在你提到的示例代码中,你只是将整个文件加载到了 ByteArrayOutputStream 中。这有什么用途?你真的需要在一个 byte[] 中存储整个文件数据吗? - Santosh
你能告诉我你打算使用哪个JDK版本吗?我有不同的解决方案针对JDK 8和JDK7或更低版本。 - Bhupi
1
@Luffy,不知道为什么要将这么多数据读入内存,回答这个问题有意义吗? - k3b
你应该按照以下答案所述增加堆大小:https://dev59.com/lHI_5IYBdhLWcg3wAeLl 但请记住,Java运行时和你的代码也需要一些空间,因此请在期望的最大值上添加一些缓冲区。 - formixian
如果输入文件大小不受限制且不在您的控制范围内,该怎么办? - OneCricketeer
不控制文件大小基本上是发布软件时的常态,但大多数情况下,您可以估算出将提供给程序的文件大小。当您无法猜测时,在其他Java商业应用程序中看到的是一份文档,解释如何配置Java运行时最大堆参数以适应您的需求。安装默认值通常设置为覆盖95%情况的堆量。 - formixian
13个回答

23

尝试使用 java.nio.MappedByteBuffer

http://docs.oracle.com/javase/7/docs/api/java/nio/MappedByteBuffer.html

您可以将文件内容映射到内存中,而无需手动复制它。高级操作系统提供内存映射功能,Java具有利用此功能的API。

如果我理解正确,内存映射不会将文件的全部内容加载到内存中(意思是“根据需要部分加载和卸载”),因此我猜一个10GB的文件不会耗尽您的内存。


14

虽然你可以增加JVM内存限制,但这是不必要的。对于处理文件来说,分配像10GB这样巨大的内存显得过度和资源密集。

目前你正在使用一个名为"ByteArrayOutputStream"的类,它保留了一个内部内存来存储数据。你代码中的这行将上次读取的2KB文件块追加到该缓冲区的末尾:

bArrStream.write(localbuffer, 0, i);

bArrStream不断增长,最终会耗尽内存。

相反,您应该重新组织算法,并以流方式处理文件:

InputStream inFileReader = channelSFtp.get(path); // file reading from ssh.
byte[] localbuffer = new byte[2048];

int i = 0;
while (-1 != (i = inFileReader.read(buffer))) {
    //Deal with the current read 2KB file chunk here
}

inFileReader.close();

7

Java虚拟机(JVM)运行时有一个固定的上限内存限制,可以通过以下方式进行修改:

java -Xmx1024m ....

例如,上面的选项(-Xmx...)将限制设置为1024兆字节。您可以根据需要进行修改(在机器、操作系统等限制范围内)。请注意,这与传统应用程序不同,后者会根据需求从操作系统中分配更多内存。
然而,更好的解决方案是重新设计您的应用程序,使您不需要一次性加载整个文件到内存中。这样,您就不必调整JVM,也不会对内存造成巨大的影响。

5

你无法一次性在内存中读取10GB的文本文件。你需要先读取X MB,对其进行处理,然后再读取下一个X MB。


5
如果他有10Gb内存和64位JVM,他可以这样做。虽然他可能不应该这么做。 - Brian Agnew
@Brian 不行。即使在64位系统下,数组中的元素大小也是有限制的。 - sigi
这取决于这是什么类型的数据 @A.P.S - sigi
1
@user2717498 - 我只是在反驳你说无法将一个10GB的文件加载到内存中的说法。例如,你可以通过存储行数组来实现。 - Brian Agnew

4

你所做的事情本质上存在问题。将整个文件读入内存始终都是一个不好的想法。如果没有一些非常惊人的硬件,你真的无法使用当前技术将10GB文件读入内存中。找到一种逐行、逐记录、分块等方式来处理它们。


将整个文件读入内存肯定是一个非常不好的想法,这在任何地方都是如此。告诉我的编辑! :-) - Brian Agnew

4

是否必须获取输出流的整个ByteArray()

byte[] data = bArrStream.toByteArray();

最好的方法是按行读取并逐行写入。您可以使用 BufferedReaderScanner 来读取下面的大文件。

import java.io.*;
import java.util.*;

public class FileReadExample {
  public static void main(String args[]) throws FileNotFoundException {
    File fileObj = new File(args[0]);

    long t1 = System.currentTimeMillis();
    try {
        // BufferedReader object for reading the file
        BufferedReader br = new BufferedReader(new FileReader(fileObj)); 
        // Reading each line of file using BufferedReader class
        String str;
        while ( (str = br.readLine()) != null) {
            System.out.println(str);
        }
    }catch(Exception err){
        err.printStackTrace();
    }
    long t2 = System.currentTimeMillis();
    System.out.println("Time taken for BufferedReader:"+(t2-t1));

    t1 = System.currentTimeMillis();
    try (
        // Scanner object for reading the file
        Scanner scnr = new Scanner(fileObj);) {
        // Reading each line of file using Scanner class
        while (scnr.hasNextLine()) {
            String strLine = scnr.nextLine();
            // print data on console
            System.out.println(strLine);
        }
    }
    t2 = System.currentTimeMillis();
    System.out.println("Time taken for scanner:"+(t2-t1));

  }
}

在上述示例中,您可以使用您的ByteArrayOutputStream替换System.out
请查看以下文章以获取更多详细信息:高效读取大文件 请参考相关SE问题:Scanner vs. BufferedReader

3

使用命令行选项-Xmx运行Java,该选项设置堆的最大大小。

详见此处


这不是一个永久性的解决方案。如果您不知道输入文件有多大,该怎么办? - OneCricketeer

3
ByteArrayOutputStream是将数据写入内存缓冲区中。如果您确实希望使用它,那么您需要在JVM堆大小设置为最大可能输入大小之后进行操作。此外,如果可能的话,您可以在开始处理之前检查输入大小以节省时间和资源。
另一种方法是使用流式解决方案,在运行时已知内存使用量(可能可以配置,但仍然在程序启动之前就已知),但是否可行取决于您的应用程序领域(因为您不能再使用内存缓冲区)以及可能的其他代码架构,如果无法/不想更改它。

3

尝试使用较大的缓冲读取大小,可以是10 MB,然后进行检查。


3
假设你正在读取大型txt文件,并且数据逐行设置,请使用逐行阅读方法。据我所知,您可以读取高达6GB或更多的数据。
// Open the file
 FileInputStream fstream = new FileInputStream("textfile.txt");
 BufferedReader br = new BufferedReader(new InputStreamReader(fstream));

  String strLine;

 //Read File Line By Line
 while ((strLine = br.readLine()) != null)   {
  // Print the content on the console
  System.out.println (strLine);
 }

 //Close the input stream
 br.close();

Refrence for the code fragment


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