为什么将文件读入内存需要4倍于Java的内存?

时间:2009-07-06 21:48:20

标签: java performance memory file file-io

我有以下代码读取跟随文件,将\ r \ n附加到每行的末尾并将结果放在字符串缓冲区中:

public InputStream getInputStream() throws Exception {
    StringBuffer holder = new StringBuffer();
    try{
        FileInputStream reader = new FileInputStream(inputPath);


        BufferedReader br = new BufferedReader(new InputStreamReader(reader));
        String strLine;
        //Read File Line By Line
        boolean start = true;
        while ((strLine = br.readLine()) != null)   {
            if( !start )    
                holder.append("\r\n");

            holder.append(strLine);
            start = false;
        }
        //Close the input stream
        reader.close();
    }catch (Throwable e){//this is where the heap error is caught up to 2Gb
      System.err.println("Error: " + e.getMessage());
    }


    return new StringBufferInputStream(holder.toString());
}

我尝试在400Mb文件中读取,并且我将最大堆空间更改为2Gb但它仍然给出了内存堆异常。有什么想法吗?

9 个答案:

答案 0 :(得分:21)

它可能与StringBuffer在达到容量时的大小调整有关 - 这涉及创建一个新的char[]倍大小的前一个,然后将内容复制到新数组中。与已经将Java中的字符存储为2个字节的点一起,这肯定会增加您的内存使用量。

要解决这个问题,您可以创建一个具有足够容量的StringBuffer,因为您知道文件大小(因此可以读取大致的字符数)。但是,请注意,如果您尝试将此大StringBuffer转换为String,也会发生数组分配。

另一点:您应该优先StringBuilder而不是StringBuffer,因为对它的操作更快。

您可以考虑使用例如{[1}} char []来实现自己的“CharBuffer”,以避免昂贵的数组分配/复制操作。你可以使这个类实现LinkedList,并且可能完全避免转换为CharSequence。关于更紧凑表示的另一个建议:如果您正在阅读包含大量重复单词的英文文本,您可以阅读并存储每个单词,使用String功能可以显着减少存储空间。

答案 1 :(得分:13)

首先,Java字符串是UTF-16(即每个字符2个字节),因此假设您的输入文件是ASCII或类似的每字符一个字节格式,那么holder将是大小的2倍输入数据,加上每行额外\r\n和任何额外开销。假设StringBuffer中的存储开销非常低,那么马上就有大约800MB。

我还可以相信文件的内容被缓冲两次 - 一次在I / O级别,一次在BufferedReader中。

但是,要确切地知道,最好查看堆上的实际内容 - 使用像HPROF这样的工具来查看内存的确切位置。

我解决这个问题的条件,我建议你一次处理一行,在你添加行终止后写出每一行。这样你的内存使用量应该与行的长度成比例,而不是整个文件。

答案 2 :(得分:12)

这是一个有趣的问题,但是为什么不尝试不需要程序将整个文件加载到内存中的设计,而不是强调Java为什么要使用这么多内存?

答案 3 :(得分:11)

这里有很多问题:

  • Unicode:字符占用内存空间的两倍(假设编码为1字节)
  • StringBuffer调整大小:可以加倍(永久)和三倍(临时)占用的内存,尽管这是最糟糕的情况
  • StringBuffer.toString()暂时将占用的内存加倍,因为它会复制

所有这些组合意味着您可以在RAM中暂时需要最多8倍的文件大小,即400M文件的3.2G。即使您的计算机在物理上拥有那么多RAM,它也必须运行64位操作系统和JVM来实际为JVM获取那么多堆。

总而言之,在内存中保留如此庞大的字符串只是一个可怕的想法 - 它也是完全不必要的 - 因为你的方法返回InputStream,所有你真正需要的是一个FilterInputStream,可以动态添加换行符。

答案 4 :(得分:4)

这是StringBuffer。空构造函数创建一个StringBuffer,初始长度为16 Bytes。现在,如果你追加某些东西并且容量不够,它会将内部字符串数组的Arraycopy作为新的缓冲区。

所以事实上,每增加一行,StringBuffer必须创建一个完整的内部数组的副本,当附加最后一行时,它几乎加倍所需的内存。与UTF-16表示一起,这会导致观察到的内存需求。

修改

迈克尔说的是,内部缓冲区不会以小部分递增 - 它的大小大致翻了一倍,因为你需要更多的内存。但是,在最坏的情况下,假设缓冲区需要在最后一次追加时扩展容量,它会创建一个比实际数量大两倍的新数组 - 所以在这种情况下,你需要大约三倍的数量记忆。

无论如何,我已经吸取了教训:StringBuffer(和Builder)可能会导致意外的OutOfMemory错误,并且我总是用一个大小初始化它,至少在我必须存储大字符串时。谢谢你的问题:)

答案 5 :(得分:1)

在最后一次插入StringBuffer时,你需要分配三倍的内存,因为StringBuffer总是扩展(size + 1)* 2(由于unicode已经加倍)。因此,400GB文件可能需要在插入结束时分配800GB * 3 == 2.4GB。它可能更少,这取决于达到阈值的确切时间。

这里建议连接字符串而不是使用缓冲区或构建器。将会有大量的垃圾收集和对象创建(因此速度很慢),但内存占用要少得多。

[在迈克尔的提示下,我进一步调查了这一点,并且concat在这里没有帮助,因为它复制了char缓冲区,所以虽然它不需要三倍,但最后需要加倍内存。]

如果你知道文件的最大大小并在创建时初始化Buffer的大小,你可以继续使用Buffer(或者更好的是在这种情况下是Builder),你确定这个方法只能从一个线程调用一次。

但实际上这种将大型文件同时加载到内存中的方法应该只是作为最后的手段来完成。

答案 6 :(得分:1)

我建议您使用操作系统文件缓存,而不是通过字符将数据复制到Java内存中,然后再返回字节。如果您根据需要重新阅读该文件(可能会随着时间进行转换),它将更快并且很可能更简单

你需要超过2 GB,因为1字节字母在内存中使用char(2字节),当你的StringBuffer调整大小时你需要加倍(将旧数组复制到更大的新数组)新数组通常大50%所以你需要最多6倍的原始文件大小。如果性能不够糟糕,那么您使用的是StringBuffer而不是StringBuilder,它可以在显然不需要时同步每个调用。 (这只会减慢你的速度,但使用相同的内存量)

答案 7 :(得分:1)

其他人已经解释了为什么你的内存不足。至于如何解决这个问题,我建议编写一个自定义的FilterInputStream子类。这个类一次读取一行,附加“\ r \ n”字符并缓冲结果。一旦您的FilterInputStream的使用者读取了该行,您就会读到另一行。这样你一次只能在内存中有一行。

答案 8 :(得分:0)

我还建议您查看Commons IO FileUtils课程。具体来说:org.apache.commons.io.FileUtils #readFileToString。如果您知道只使用ASCII,也可以指定编码。