保留java中字符串的堆大小

时间:2011-12-08 08:02:14

标签: java string memory profiling retained-in-memory

这是一个我们无法理解的问题。用文字描述它是很棘手的,但我希望能够理解这个要点。

我理解字符串的实际内容包含在内部char数组中。在正常情况下,字符串的保留堆大小将包括40个字节加上字符数组的大小。这是here的解释。调用子字符串时,字符数组会保留对原始字符串的引用,因此字符数组的保留大小可能比字符串本身大很多。

然而,当使用Yourkit或MAT分析内存使用情况时,似乎发生了一些奇怪的事情。引用char数组保留大小的字符串不包括字符数组的保留大小。

一个例子如下(半伪代码):

String date = "2011-11-33"; (24 bytes)
date.value = char{1172}; (2360 bytes)

字符串的保留大小定义为24个字节,不包括字符数组的保留大小。如果由于许多子串操作而导致对字符数组的引用很多,那么这是有意义的。

现在,当此字符串包含在某种类型的集合(如数组或列表)中时,此数组的保留大小将包括所有字符串的保留大小,包括字符数组的保留大小。

然后我们有这样的情况:

Array's retained size = 300 bytes
array[0] = String 40 bytes;
array[1] = String 40 bytes;
array[1].value = char[] (220 bytes)

因此,您必须查看每个数组条目,以尝试确定保留大小的来源。

同样可以解释这个数组包含所有保存对同一字符数组的引用的字符串,因此数组的保留大小总是正确的。

现在我们解决了这个问题。

我在一个单独的对象中保留了对上面讨论的数组的引用以及具有相同字符串的不同数组。在两个数组中,字符串引用相同的字符数组。这是预期的 - 毕竟我们正在讨论相同的字符串。但是,此字符数组的保留大小将计入此新对象中的两个数组。换句话说,保留的大小似乎是双倍的。如果我删除第一个数组,那么第二个数组仍将保持对字符数组的引用,反之亦然。这引起了混淆,因为似乎java对同一个字符数组持有两个单独的引用。怎么会这样?这是java内存的问题还是仅仅是分析器显示信息的方式?

这个问题让我们在尝试追踪应用程序中的大量内存时引起了很多麻烦。

再一次 - 我希望那里的人能够理解这个问题并解释它。

感谢您的帮助

4 个答案:

答案 0 :(得分:4)

  

我在一个单独的对象中保留了对上面讨论的数组的引用以及具有相同字符串的不同数组。在两个数组中,字符串引用相同的字符数组。这是预期的 - 毕竟我们正在讨论相同的字符串。但是,此字符数组的保留大小将计入此新对象中的两个数组。换句话说,保留的大小似乎是双倍。

您在这里拥有的是一个传递参考在

enter image description here

字符数组不应显示在任一数组的保留大小中。如果探查器以这种方式显示它,那就是误导。

这就是JProfiler在最大对象视图中显示这种情况的方式:

enter image description here

两个数组中包含的字符串实例显示在数组实例外部,带有[传递引用]标签。如果要探索实际路径,可以将数组持有者和字符串添加到图形中,并找到它们之间的所有路径:

enter image description here

免责声明:我公司开发JProfiler。

答案 1 :(得分:3)

我说这只是探查器显示信息的方式。它不知道应该考虑将两个数组用于“重复数据删除”。你如何将两个数组包装成某种虚拟持有者对象,并运行你的探查器?然后,它应该能够处理“重复计算”。

答案 2 :(得分:0)

除非字符串被实习,否则它们可以是equal()但不是==。从char数组构造String对象时,构造函数将生成char数组的副本。 (这是保护不可变String在以后更改char数组值的唯一方法。)

答案 3 :(得分:0)

如果您使用-XX:-UseTLAB

运行
public static void main(String... args) throws Exception {
    StringBuilder text = new StringBuilder();
    text.append(new char[1024]);
    long free1 = free();
    String str = text.toString();
    long free2 = free();
    String [] array = { str.substring(0, 100), str.substring(101, 200) };
    long free3 = free();
    if (free3 == free2)
        System.err.println("You must use -XX:-UseTLAB");
    System.out.println("To create String with 1024 chars "+(free1-free2)+" bytes\nand to create an array with two sub-string was "+(free2-free3));
}

private static long free() {
    return Runtime.getRuntime().freeMemory();
}

打印

To create String with 1024 chars 2096 bytes
and to create an array with two sub-string was 88

如果他们共享相同的后端存储,您可以看到它消耗的内存更多。

如果您查看字符串类中的代码。

public String substring(int start, int end) {
    // checks.
    return ((beginIndex == 0) && (endIndex == count)) ? this :
        new String(offset + beginIndex, endIndex - beginIndex, value);
}

String(int offset, int count, char value[]) {
    this.value = value;
    this.offset = offset;
    this.count = count;
}

您可以看到 String 的子字符串不会获取基础值数组的副本。


另一件需要考虑的事情是-XX:+UseCompressedStrings默认情况下在较新版本的JVM上启用。这鼓励JVM尽可能使用byte []而不是char []。

对于32位JVM,具有32位引用的64位JVM和具有64位引用的64位JVM,String和数组对象的标头大小各不相同。