【问题标题】:Java not garbage collecting memoryJava不是垃圾收集内存
【发布时间】:2012-06-12 16:30:49
【问题描述】:

我正在阅读一个非常大的文件并从每一行中提取一些文本的一小部分。然而,在手术结束时,我只剩下很少的内存可以使用了。似乎垃圾收集器在读入文件后无法释放内存。

我的问题是:有没有办法释放这个内存?或者这是一个 JVM 错误?

我创建了一个 SSCCE 来证明这一点。它读取一个 1 mb(Java 中为 2 mb,由于 16 位编码)文件,并从每行中提取一个字符(约 4000 行,因此应该约为 8 kb)。在测试结束时,仍然使用完整的 2 mb!

初始内存使用量:

Allocated: 93847.55 kb
Free: 93357.23 kb

读入文件后(在任何手动垃圾收集之前):

Allocated: 93847.55 kb
Free: 77613.45 kb (~16mb used)

这是意料之中的,因为程序正在使用大量资源来读取文件。

然后我进行垃圾收集,但并非所有内存都被释放:

Allocated: 93847.55 kb
Free: 91214.78 kb (~2 mb used! That's the entire file!)

我知道手动调用垃圾收集器并不能为您提供任何保证(在某些情况下它是懒惰的)。然而,这发生在我较大的应用程序中,其中文件几乎耗尽了所有可用内存,并导致程序的其余部分内存不足,尽管需要它。这个例子证实了我的怀疑,从文件中读取的多余数据没有被释放。

这里是生成测试的 SSCCE:

import java.io.*;
import java.util.*;

public class Test {
    public static void main(String[] args) throws Throwable {
        Runtime rt = Runtime.getRuntime();

        double alloc = rt.totalMemory()/1000.0;
        double free = rt.freeMemory()/1000.0;

        System.out.printf("Allocated: %.2f kb\nFree: %.2f kb\n\n",alloc,free);

        Scanner in = new Scanner(new File("my_file.txt"));
        ArrayList<String> al = new ArrayList<String>();

        while(in.hasNextLine()) {
            String s = in.nextLine();
            al.add(s.substring(0,1)); // extracts first 1 character
        }

        alloc = rt.totalMemory()/1000.0;
        free = rt.freeMemory()/1000.0;
        System.out.printf("Allocated: %.2f kb\nFree: %.2f kb\n\n",alloc,free);

        in.close();
        System.gc();

        alloc = rt.totalMemory()/1000.0;
        free = rt.freeMemory()/1000.0;
        System.out.printf("Allocated: %.2f kb\nFree: %.2f kb\n\n",alloc,free);
    }
}

【问题讨论】:

  • 除非你正在做一些其他人不可能做的非常不寻常的事情,否则“jvm bug”不应该是你的第一个假设。
  • 您希望 System.gc() 如何释放所有内存?您仍在使用 al 中的字符串,因此无法释放它们。
  • @PaulTomblin:我研究这个问题已经有一段时间了,但没有任何结果。而且我看不出有什么好的理由说明为什么会发生这种情况。
  • @dystroy 的回答一针见血。 substring 并没有创建一个全新的字符串,它包含对原始字符串的引用。
  • substring 确实保留了原始字符数据。

标签: java performance memory garbage-collection


【解决方案1】:

确保不再保留不再需要的参考资料。

您仍然有对alin 的引用。

在调用垃圾收集器之前尝试添加al = null; in = null;

另外,你需要了解substring 是如何实现的。 substring保留原始字符串,只是对同一个char[]数组使用不同的偏移量和长度。

al.add(new String(s.substring(0,1)));

不确定是否有更优雅的复制子字符串的方法。也许s.getChars() 对你也更有用。

从 Java 8 开始,子字符串 现在复制字符。你可以自己验证构造函数是否调用了Arrays.copyOfRange

【讨论】:

  • 我需要使用al 中的字符串。我只是希望文件的其余部分被垃圾收集。
  • 至少在我的 JDK 中,new String 会在长度不同时修剪字符串。
【解决方案2】:

创建子字符串时,您的子字符串保留对原始字符串的 char 数组的引用(这种优化使得处理字符串的许多子字符串非常快)。因此,当您将子字符串保存在 al 列表中时,您会将整个文件保存在内存中。为避免这种情况,请使用将字符串作为参数的构造函数创建一个新字符串。

所以基本上我建议你这样做

    while(in.hasNextLine()) {
        String s = in.nextLine();
        al.add(new String(s.substring(0,1))); // extracts first 1 character
    }

String(String) 构造函数的源代码明确指出它的用途是修剪“包袱”:

  164       public String(String original) {
  165           int size = original.count;
  166           char[] originalValue = original.value;
  167           char[] v;
  168           if (originalValue.length > size) {
  169               // The array representing the String is bigger than the new
  170               // String itself.  Perhaps this constructor is being called
  171               // in order to trim the baggage, so make a copy of the array.
  172               int off = original.offset;
  173               v = Arrays.copyOfRange(originalValue, off, off+size);
  174           } else {
  175               // The array representing the String is the same
  176               // size as the String, so no point in making a copy.
  177               v = originalValue;
  178           }
  179           this.offset = 0;
  180           this.count = size;
  181           this.value = v;

更新:这个问题在 OpenJDK 7 更新 6 中消失了。使用更新版本的人没有这个问题。

【讨论】:

  • 嗯...有趣。这是 substring 所做的一个奇怪的优化。但它解释了发生了什么。似乎还有一个关于它的错误报告:bugs.sun.com/bugdatabase/view_bug.do?bug_id=4513622
  • 我记得,这出现在 java (1.02) 的第一个版本中,当时它被视为一种智能优化。问题在于它使垃圾处理变得更加复杂。
  • 我可以看到它背后的原因,因为它将子字符串减少到 O(1) 操作。但这对我来说几乎像是内存泄漏。
  • @assylias 查看 String(String) 构造函数的源代码:你会发现它是专门为此创建的。
【解决方案3】:

System.gc() 不能保证 JVM 会进行垃圾收集 - 它只是建议 JVM 可以尝试进行垃圾收集。由于已经有大量可用内存,JVM 可能会忽略该建议并继续运行,直到它觉得需要这样做为止。

在文档http://docs.oracle.com/javase/6/docs/api/java/lang/System.html#gc()了解更多信息

另一个关于它的问题可以在When does System.gc() do anything找到

【讨论】:

    猜你喜欢
    • 2012-06-28
    • 1970-01-01
    • 2014-03-31
    • 2019-05-15
    • 2010-12-13
    • 1970-01-01
    • 1970-01-01
    • 2013-09-29
    • 2011-11-07
    相关资源
    最近更新 更多