【问题标题】:Java OutOfMemoryError in reading a large text fileJava OutOfMemoryError 读取大文本文件
【发布时间】:2013-09-01 18:17:51
【问题描述】:

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

任何帮助将不胜感激?

【问题讨论】:

  • 在您提到的示例代码中,您只是将整个文件加载到ByteArrayOutputStream 中。用例是什么?真的需要byte[] 中的整个文件数据吗?
  • 你能告诉我你打算使用哪个 JDK 版本吗,我对 JDK 8 和 JDK7 或更低版本有不同的解决方案。
  • @Luffy 在不知道为什么这么多数据被读入内存的情况下回答这个问题有意义吗?
  • 以下任何答案是否有助于解决您的问题?如果它有帮助,请接受它以帮助有类似问题的人。如果没有帮助,请告诉我,以便我删除我的,以免人们浪费时间查看它。

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


【解决方案1】:

尝试使用 java.nio.MappedByteBuffer

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

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

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

【讨论】:

    【解决方案2】:

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

    【讨论】:

      【解决方案3】:

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

      java -Xmx1024m ....
      

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

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

      【讨论】:

        【解决方案4】:

        您无法读取内存中的 10GB 文本文件。您必须先读取 X MB,然后对其进行处理,然后再读取下一个 X MB。

        【讨论】:

        • 如果他有 10Gb 和 64 位 JVM,他可以做到这一点。不过,他可能不应该这样做。
        • @Brian 不,他不能。即使在 64 位下,数组中元素的大小也是有限的。
        • 取决于这是什么类型的数据@A.P.S
        • @user2717498 - 我只是在反驳您不能在内存中加载 10Gb 文件的断言。您可以通过存储行数组来做到这一点,例如
        【解决方案5】:

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

        See here for details..

        【讨论】:

        • 这个链接不适合我,除了链接之外,你能把重要的信息放在这里吗?
        【解决方案6】:

        尝试使用较大的缓冲区,读取大小可能为 10 mb,然后检查。

        【讨论】:

          【解决方案7】:

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

          【讨论】:

          • “将整个文件读入内存总是一个坏主意”?告诉我的编辑! :-)
          【解决方案8】:

          是否必须获取整个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

          【讨论】:

            【解决方案9】:

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

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

            【讨论】:

              【解决方案10】:

              您好,我假设您正在读取大型 txt 文件并且数据是逐行设置的,请使用逐行读取方法。据我所知,您最多可以读取 6GB 可能更多。我强烈建议您尝试这种方法。

              数据1 数据2 ...

              // 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

              【讨论】:

                【解决方案11】:

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

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

                }

                【讨论】:

                  【解决方案12】:

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

                  Increase heap size in Java

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

                  【讨论】:

                    【解决方案13】:

                    简答,

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

                    长答案:

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

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

                    【讨论】:

                      【解决方案14】:

                      想了想,我决定放第二个答案。我考虑了提出第二个答案的优点和缺点,优点是值得一试。就是这样。

                      大多数建议的考虑因素都忘记了一个给定的事实:Java 中的数组大小(包括ByteArrayOutputStream)存在内置限制。该限制由最大的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<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
                      

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

                      【讨论】:

                        猜你喜欢
                        • 1970-01-01
                        • 2019-08-17
                        • 2014-09-30
                        • 2015-06-21
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2012-10-11
                        • 1970-01-01
                        相关资源
                        最近更新 更多