【问题标题】:Java - GC a large stringJava - GC 一个大字符串
【发布时间】:2015-10-24 09:50:49
【问题描述】:

我有一种方法可以读取和解析一个极长的 xml 文件。 xml 文件被读入一个字符串,然后由不同的类解析。但是,这会导致 Java 使用大量内存(~500 MB)。 通常,程序运行在 30 MB 左右,但是当调用 parse() 时,它会增加到 500 MB。但是,当 parse() 运行完毕后,内存使用量不会降到 30 MB;相反,它保持在 500 MB。

我已尝试设置 s = null 并调用 System.gc(),但内存使用量仍保持在 500 MB。

public void parse(){
        try {
            System.out.println("parsing data...");
            String path = dir + "/data.xml";
            InputStream i = new FileInputStream(path);
            BufferedReader reader = new BufferedReader(new InputStreamReader(i));
            String line;
            String s = "";
            while ((line = reader.readLine()) != null){
                s += line + "\n";
            }

            ... parse ...

        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
}

有什么想法吗?

谢谢。

【问题讨论】:

  • 不要连接字符串。请改用StringBuilder
  • 那个“... parse ...”为其他内存使用留下了很多空间。你确定字符串 s 是你的问题吗?
  • @DonRoby 是的,我确定,因为我删除了其他所有内容,但问题仍然存在。另外,我已经评论了循环,问题就消失了。
  • @PM77-1 谢谢,帮助很大。内存使用量下降到大约 90 MB。虽然完成后它不会返回到 30 MB,但只要我不调用 parse() 100 次,我应该没问题!
  • @JasonYang:内存使用量下降,因为它现在创建的字符串实例更少。但是,除非您关闭流,否则存在内存泄漏。

标签: java memory memory-management memory-leaks garbage-collection


【解决方案1】:

内存泄漏解决方案

您应该在末尾Close BufferReader 以关闭流并释放与其关联的任何系统资源。您可以同时关闭InputStreamBufferReader。然而,关闭BufferReader 实际上也关闭了它的流。

一般情况下最好加上finally并关闭它。

finally 
{
   i.Close();
   reader.Close();
}

更好的方法try-with-resources Statement

try (BufferedReader br = new BufferedReader(new FileReader(path))) 
{
        return br.readLine();
}

奖金说明

使用 StringBuilder 而不是连接字符串

String 不允许追加。 String 上的每个附加/连接都会创建一个新对象并返回它。这是因为 Stringimmutable - 它无法更改其内部状态。

另一方面,StringBuilder 是可变的。当您调用Append 时,它会更改内部字符数组,而不是创建一个新的字符串对象。

因此,当您想要附加许多字符串时,使用 StringBuilder 会更节省内存。

【讨论】:

    【解决方案2】:

    请注意:try-with-resources 块将对您像那些阅读器这样的 IO 对象有很大帮助。

    try(InputStream i = new FileInputStream(path);
        BufferedReader reader = new BufferedReader(new InputStreamReader(i))) {
        //your reading here
    }
    

    这将确保通过对它们调用 close() 来处理这些对象,无论您的方法块如何退出(成功、异常...)。关闭这些对象也可能有助于释放一些内存。

    不过,可能导致内存使用量大幅下降和爆炸的原因是您的字符串连接。调用s += line + "\n" 对单个连接很好,但+ 运算符实际上每次都必须创建一个新的String 实例,并从被连接的字符中复制字符。 StringBuilder 类就是为此目的而设计的。 :)

    【讨论】:

    • 实现注意事项:StringBuilder 没有与使用String 重复串联使用+ 相同的问题是因为底层数据是可变的,并且相同的对象用于建立字符串。细节是有一个 char 数组的底层集合(据我所知),它保存要以块构建的字符串。这意味着这些字符中的每一个被复制的唯一时间是 1) 当 append() 被调用到 StringBuilder 和 2) 当 toString() 被调用时,而不是循环的每次迭代一次。
    • 您在使用 + 进行字符串连接和 StringBuilder 之间描述的不同之处在 Java 的早期版本中是正确的,但不再是正确的。见:stackoverflow.com/a/47628/1057429
    • @alfasin 我在该答案中看不到任何支持您的主张的内容。
    • @EJP 我的老朋友,感谢您加入我们!该答案表明a+=b 编译成的字节码相当于创建StringBuilder 并使用其append 方法。
    • @alfasin 这绝对是正确的,在多个追加在代码中的同一表达式中的情况下。这意味着使用 StringBuilder 执行 "a" + "b" + "c" + ... 不会降低性能。当我们迭代时会出现问题 - 使用内置的+ 运算符每次创建一个新的StringBuilder。这可能会导致巨大的性能损失。对于编译器来说,甚至试图优化这种事情,这将是一项艰巨的任务,超出了职责范围。
    【解决方案3】:

    500MB 是由解析引起的,所以它与字符串无关,也与BufferedReader 无关。它是解析后的 XML 的 DOM。释放它,您的内存使用将恢复。

    但是为什么要把整个文件读成一个字符串呢?这是浪费时间和空间。直接从文件中解析输入即可。

    【讨论】:

    • 这是一种猜测。考虑到问题下方的 cmets,可能是错误的。
    【解决方案4】:

    您应该记住,调用System.gc(); 不一定会进行垃圾收集,但它会建议 GC 来做这件事,如果 GC 不想进行垃圾收集,它可以忽略这样做。最好使用 StringBuilder 确实减少您在内存中创建的字符串数量,因为它仅在您调用 toString() 时才创建字符串。

    【讨论】:

      猜你喜欢
      • 2013-04-01
      • 1970-01-01
      • 1970-01-01
      • 2015-02-07
      • 2020-01-29
      • 1970-01-01
      • 2013-03-16
      • 2016-10-25
      • 2016-01-03
      相关资源
      最近更新 更多