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

ero*_*ppa 11 java memory performance file-io file

我有以下代码读取跟随文件,将\ 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());
}
Run Code Online (Sandbox Code Playgroud)

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

Ada*_*ski 21

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

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

另一点:你通常应该赞成StringBuilder,StringBuffer因为它上面的操作更快.

您可以考虑实现自己的"CharBuffer",例如使用LinkedListchar []来避免昂贵的数组分配/复制操作.您可以创建此类实现CharSequence,也许可以避免String完全转换为.更简洁表示的另一个建议:如果您正在阅读包含大量重复单词的英文文本,您可以阅读并存储每个单词,使用该String.intern()功能可以显着减少存储空间.

  • 所以旧阵列是1GB,旧阵列满了,创建新阵列2GB拷贝1GB阵列到2GB阵列(但你手上当前有3GB内存)1GB丢失参考等待垃圾收集,2GB阵列成为新存储,它仍然剩下空间(从第一个1GB从旧数组复制后为1GB)开始使用. (3认同)

Dav*_*veR 13

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

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

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

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


Chr*_*Rea 12

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

  • 我很惊讶我对此回应投了反对票.实际上,有时候我们开发人员会浪费时间来弄清楚为什么特定的做事方式不能像我们希望的那样工作,而我们应该退一步尝试不同的方法.我想任何时候处理非常大的文件并将整个内容加载到内存中,第一个问题应该是"为什么?" (14认同)
  • 当开发人员要求解决方案时,显然有一个原因.不要以为每个问题都来自高中生. (14认同)
  • 这不是一个答案,而是一个有用的评论.它应该放在评论部分而不是答案部分,不应该被投票(因为它没有解决问题)http://bit.ly/MohSi (13认同)
  • 你不必成为一名高中学生就会陷入困境,错过更大的图片/替代解决方案. (5认同)
  • 让我这样说吧; 如果我问一个问题,有些SO用户说"嘿,你开始做错了,试试这个!",我这样做,而且效果很好,我很高兴. (4认同)
  • @erotsppa:那么......是什么原因? (3认同)
  • 看一下方法的返回值 - 这种方法在几乎100%的确定性方面是根本错误的,这是唯一明智的答案. (3认同)

Mic*_*rdt 11

你有很多问题:

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

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

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