Java OutOfMemoryError在读取大文本文件时

时间:2013-08-29 10:45:20

标签: java arrays file-io out-of-memory bytearray

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

任何帮助都会受到赞赏吗?

14 个答案:

答案 0 :(得分:18)

尝试使用 java.nio.MappedByteBuffer

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

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

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

答案 1 :(得分:12)

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

答案 2 :(得分:7)

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

java -Xmx1024m ....

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

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

答案 3 :(得分:5)

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

See here for details..

答案 4 :(得分:4)

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

答案 5 :(得分:4)

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

答案 6 :(得分:4)

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

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

  }
}

您可以在上面的示例中将System.out替换为ByteArrayOutputStream

请查看以下文章了解更多详情:Read Large File

看看相关的SE问题:

Scanner vs. BufferedReader

答案 8 :(得分:3)

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

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

答案 9 :(得分:3)

您好我假设您正在阅读大型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();

Refrence for the code fragment

答案 10 :(得分:3)

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

  

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

由Apache Commons IO提供。

@Entity
public class EntityClass{
   @Id
   @GeneratedValue(strategy=GenerationType.AUTO)
   public int id;
   public EntityClass() {
   } 
   ...
}

}

答案 11 :(得分:2)

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

Increase heap size in Java

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

答案 12 :(得分:2)

简短回答,

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

答案很长:

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

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

答案 13 :(得分:0)

在考虑之后,我决定回答第二个问题。我考虑了第二个答案的优点和缺点,其优点是值得的。所以就是这样。

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

我的建议是使用ArrayList byte[]而不是一个byte[]来保存文件的完整内容。并且在将ByteArrayOutputStream放入最终的data数组之前,还要删除放入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<BUF_SIZE){ data.add( Arrays.copyOf(localbuffer, i) ) // No need to reallocate the reading buffer, we copied the data }else{ data.add(localbuffer) // reallocate the reading buffer localbuffer = new byte[BUF_SIZE] } } inFileReader.close(); // Process your data, keep in mind that you have a list of buffers. // So you need to loop over the list 的非必要步骤。以下是基于原始代码的示例:

-Xms

只需运行程序就可以在具有足够物理内存或交换的64位系统上正常运行。现在,如果您想加快速度以帮助VM在开始时正确地填充堆,请使用选项-Xmxjava -Xms12288m -Xmx12288m YourApp运行。例如,如果您希望12GB的堆能够处理10GB文件,请使用=IF(IF(A4 = "",INDIRECT(ADDRESS(5,1 + COUNTIF($A$4:A4,"") - 1)),A4)=0,"",IF(A4 = "",INDIRECT(ADDRESS(5,1 + COUNTIF($A$4:A4,"") - 1)),A4))