Java - 处理大字符串的垃圾回收

3
我有一种方法可以读取和解析非常长的xml文件。将xml文件读入字符串,然后由另一个类解析该字符串。 然而,这会导致Java使用大量内存(约500MB)。通常,程序运行时占用30MB左右,但调用parse()方法后,它会增加到500MB。然而,当parse()运行完毕时,内存使用量不会回到30MB,而是保持在500MB。
我尝试设置s = null并调用System.gc(),但内存使用量仍然保持在500MB。
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();
        }
}

有什么想法吗?

谢谢。


1
不要连接字符串。使用 StringBuilder - PM 77-1
1
“...解析…”留下了很多空间供其他内存使用。你确定字符串s是问题所在吗? - Don Roby
@DonRoby 是的,我确定,因为我删除了其他所有内容,但问题仍然存在。此外,我已经注释掉了循环,问题消失了。 - Jason Yang
1
@PM77-1 谢谢,这帮了我很多。内存使用量降到了约90 MB左右。虽然完成后它不会返回到30 MB,但只要我不调用parse() 100次,我应该没问题! - Jason Yang
@JasonYang:由于现在创建的字符串实例较少,内存使用量有所下降。然而,除非关闭流,否则会存在内存泄漏问题。 - CharithJ
4个回答

2

解决内存泄漏问题的方法

您应该在结尾处关闭BufferReader,以关闭流并释放与其关联的任何系统资源。您可以关闭InputStreamBufferReader。但是,关闭BufferReader实际上也会关闭其流。

通常最好添加一个finally语句块并关闭它。

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

更好的处理方式是使用try-with-resources语句。 点击此处 了解更多信息。
try (BufferedReader br = new BufferedReader(new FileReader(path))) 
{
        return br.readLine();
}

额外提示

使用StringBuilder而非字符串连接

String 不支持追加。每个在 String 上的追加 / 连接操作都会创建一个新对象并返回它。这是因为 String 是不可变的 - 它无法改变其内部状态。

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

因此,在需要追加许多字符串时,使用 StringBuilder 更具有内存效率。


0

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

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


这只是一种猜测,考虑到问题下面的评论,很可能是错误的。 - Nir Alfasi

0
你应该记住,调用 System.gc(); 并不一定会执行垃圾回收,但它建议 GC 执行其任务,如果 GC 不想进行垃圾回收,则可以忽略该操作。最好使用 StringBuilder 来减少在内存中创建的字符串数量,因为它只在调用 toString() 时才创建字符串。

0

注意: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() 被调用时,而不是在循环的每次迭代中都复制一次。 - Oly
你描述的在字符串连接中使用 + 和 StringBuilder 之间的区别在早期版本的Java中是正确的,但现在不再是这样。请参见:https://dev59.com/JnVD5IYBdhLWcg3wOo9h#47628 - Nir Alfasi
@alfasin,我在那个答案中没有看到任何支持你的说法的内容。 - user207421
@EJP老朋友,感谢您的加入!这个答案表明,a+=b编译成的字节码等同于创建一个StringBuilder并使用它的append方法。 - Nir Alfasi
1
@alfasin 这绝对是正确的,在代码中多次追加的情况下是如此。这意味着使用"a" + "b" + "c" + ...与使用StringBuilder相比,并没有性能惩罚。问题出现在迭代时 - 使用内置的 + 运算符每次都会创建一个新的 StringBuilder。这可能会导致巨大的性能损失。对于编译器来说,甚至尝试优化这种情况将是一项困难的任务,超出了职责的范围。 - Oly
我错过了你提到循环的事实。没错! - Nir Alfasi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接