Java OutOfMemoryError在读取大文本文件时

我是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) 

任何帮助,将不胜感激?

尝试使用java.nio.MappedByteBuffer

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

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

如果我的理解是正确的,内存映射不会将文件的整个内容加载到内存中(意思是“根据需要部分加载和卸载”),所以我猜一个10GB的文件不会占用你的内存。

即使你可以增加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(); 

Java虚拟机(JVM)以固定的内存上限运行,您可以对其进行修改:

 java -Xmx1024m .... 

例如,上面的选项(-Xmx …)将限制设置为1024兆字节。 您可以根据需要进行修改(在您的机器,操作系统等的限制范围内)。请注意,这与传统应用程序不同,后者将根据需要从操作系统分配越来越多的内存。

但是,更好的解决方案是重新编写应用程序,这样您就不需要一次性将整个文件加载到内存中。 这样您就不必调整JVM,也不会占用大量内存。

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

请看这里详细..

您无法在内存中读取10GB文本文件。 你必须首先读取X MB,用它做一些事情然后阅读下一个X MB。

尝试使用大缓冲区读取大小可能是10 MB然后检查。

问题是你正在做的事情所固有的。 将整个文件读入内存始终是一个坏主意。 除非你有一些令人吃惊的硬件,否则你真的无法用现有技术将10GB文件读入内存。 找到一种方法来逐行处理它们,按记录记录,按块块处理,…

是否必须获取输出流的整个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问题:

扫描仪与BufferedReader

ByteArrayOutputStream写入内存缓冲区。 如果这确实是您希望它工作的方式,则必须在输入的最大可能大小之后调整JVM堆的大小。 此外,如果可能,您可以在开始处理之前检查输入大小以节省时间和资源。

另一种方法是流式解决方案,其中运行时使用的内存量是已知的(可能是可配置的,但在程序启动之前仍然可以知道),但是如果它可行或不可行完全取决于您的应用程序的域(因为您不能使用内存缓冲区了)如果您不想/不想更改它,可能还有其余代码的架构。

嗨我假设您正在读取大型txt文件并逐行设置数据,使用逐行读取方法。 据我所知,你可以阅读高达6GB的内容。 我强烈建议你尝试这种方法。

DATA1 DATA2 ……

 // 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(); 

参考代码片段

按行迭代读取文件。 这将显着减少内存消耗。 或者你可以使用

FileUtils.lineIterator(theFile,“UTF-8”);

由Apache Commons IO提供。

 FileInputStream inputStream = null; Scanner sc = null; try { inputStream = new FileInputStream(path); sc = new Scanner(inputStream, "UTF-8"); while (sc.hasNextLine()) { String line = sc.nextLine(); // System.out.println(line); } // note that Scanner suppresses exceptions if (sc.ioException() != null) { throw sc.ioException(); } } finally { if (inputStream != null) { inputStream.close(); } if (sc != null) { sc.close(); } 

}

您应该按照以下答案中的说明增加堆大小:

增加Java中的堆大小

但请记住,Java运行时和代码也会占用一些空间,因此请将一些缓冲区添加到所需的最大值。

简短的回答,

没有做任何事情,你可以将电流限制推高1.5倍。 这意味着,如果您能够处理800MB,则可以处理1200 MB。 这也意味着如果通过java -Xm ....的一些技巧java -Xm ....你可以移动到当前代码可以处理7GB的点,你的问题就解决了,因为1.5因素将带你到10.5GB,假设你有系统上可用的空间,JVM可以获得它。

答案很长:

该错误非常具有自我描述性。 您达到了配置的实际内存限制。 有很多关于你可以使用JVM的限制的猜测,我对此知之甚少,因为我找不到任何官方信息。 但是,您将以某种方式受限于可用交换,内核地址空间使用,内存碎片等约束。

现在发生的是,如果你不提供任何大小(这是你的情况), ByteArrayOutputStream对象是用大小为32的默认缓冲区创建的。 无论何时在对象上调用write方法,都会启动一个内部机制。 openjdk实现版本7u40-b43似乎与您的错误输出完全匹配,使用内部方法ensureCapacity来检查缓冲区是否有足够的空间放置您想要写入的字节。 如果没有足够的空间,则调用另一个内部方法grow来增加缓冲区的大小。 方法grow定义了适当的大小,并从类Arrays调用方法copyOf来完成工作。 缓冲区的适当大小是当前大小和保持所有内容(当前内容和要写入的新内容)所需的大小之间的最大值。 类Arrays的方法copyOf ( 按照链接 )为新缓冲区分配空间,将旧缓冲区的内容复制到新缓冲区并返回grow

在为新缓冲区分配空间时会出现问题。在进行一些write ,您将达到可用内存耗尽的程度: java.lang.OutOfMemoryError: Java heap space

如果我们查看详细信息,您将阅读2048年的大块。所以

  • 你的第一次写入增加缓冲区的大小从32到2048
  • 你的第二个电话将它加倍到2 * 2048
  • 你的第三个电话会把它带到2 ^ 2 * 2048,你需要在需要分配之前再写两次。
  • 那么2 ^ 3 * 2048,你将有时间再分配4次以上。
  • 在某些时候,你的缓冲区大小为2 ^ 18 * 2048,即2 ^ 19 * 1024或2 ^ 9 * 2 ^ 20(512 MB)
  • 然后是2 ^ 19 * 2048,即1024 MB或1 GB

您的描述中不清楚的一点是,您可以以某种方式读取高达800MB,但不能超越。 你必须向我解释一下。

我希望你的极限正好是2的幂(或者如果我们使用10个单位的功率则关闭)。 在这方面,我希望你立即开始遇到麻烦,其中一个:256MB,512 MB,1GB,2GB等。

当你达到这个限制时,并不意味着你的内存不足,它只是意味着不可能分配另一个缓冲区,它的大小是你已经拥有的缓冲区的两倍。 这一观察结果为您的工作提供了改进空间:找到可以分配的最大缓冲区大小,并通过调用相应的构造函数预先保留它

 ByteArrayOutputStream bArrStream = new ByteArrayOutputStream(myMaxSize); 

它的优点是可以减少在引擎盖下发生的开销后台内存分配,让您满意。 通过这样做,您将能够达到1.5现在的限制。 这只是因为缓冲区的最后一次增加,它从当前大小的一半变为当前大小,并且在某些时候你将当前缓冲区和旧缓冲区一起放在内存中。 但是你不能超过你现在的限制的3倍。 解释完全一样。

话虽如此,我没有任何神奇的建议来解决问题,除了通过给定大小的块处理你的数据,一次一块。 另一个好方法是使用Takahiko Kawasaki的建议并使用MappedByteBuffer 。 请记住,在任何情况下,您都需要至少10 GB的物理内存或交换内存才能加载10GB的文件。

看到

在考虑之后,我决定再做一个答案。 我考虑了第二个答案的优点和缺点,其优点是值得的。 所以在这里。

大多数建议的注意事项都忘记了一个特定的事实:Java中可以包含的数组(包括ByteArrayOutputStream )的内置限制。 而这个限制是由bigest int值决定的,即2 ^ 31 – 1(略小于2Giga)。 这意味着您最多只能读取2 GB(-1字节)并将其放在一个ByteArrayOutputStream 。 如果VM需要更多控制,则数组大小的限制实际上可能更小。

我的建议是使用byte[]ArrayList而不是保存文件全部内容的单个byte[] 。 并且在将其放入最终data数组之前,还要删除放入ByteArrayOutputStream的非必要步骤。 以下是基于原始代码的示例:

 InputStream inFileReader = channelSFtp.get(path); // file reading from ssh. // good habits are good, define a buffer size final int BUF_SIZE = (int)(Math.pow(2,30)); //1GB, let's not go close to the limit byte[] localbuffer = new byte[BUF_SIZE]; int i = 0; while (-1 != (i = inFileReader.read(localbuffer))) { if(i 

只需运行程序就可以在64位系统上正常工作,并具有足够的物理内存或交换。 现在,如果您想加速它以帮助VM在开始时正确地调整堆大小,请使用-Xms-Xmx选项运行。 例如,如果您希望12GB的堆能够处理10GB文件,请使用java -Xms12288m -Xmx12288m YourApp