7

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

我知道字符串的实际内容包含在内部 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

4 回答 4

4

我在一个单独的对象中保存了对上面讨论的数组的引用以及具有相同字符串的不同数组。在这两个数组中,字符串引用相同的字符数组。这是意料之中的——毕竟我们谈论的是同一个字符串。然而,这个字符数组的保留大小被计算在这个新对象中的两个数组中。换句话说,保留的大小似乎是两倍。

您在这里拥有的是支配树中的传递引用

在此处输入图像描述

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

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

在此处输入图像描述

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

在此处输入图像描述

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

于 2011-12-08T10:52:46.037 回答
3

我想说这只是分析器显示信息的方式。它不知道应该考虑对这两个数组进行“重复数据删除”。您如何将这两个数组包装到某种虚拟持有者对象中,然后针对该对象运行您的分析器?然后,它应该能够处理“双重计算”。

于 2011-12-08T08:11:31.690 回答
0

除非字符串是 interned,否则它们可以是equal()但不是==. 从 char 数组构造 String 对象时,构造函数将复制 char 数组。(这是保护不可变字符串免受 char 数组值以后更改的唯一方法。)

于 2011-12-08T08:10:36.370 回答
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

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

如果您查看String类中的代码。

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,字符串和数组对象的标头大小会有所不同。

于 2011-12-08T08:14:39.630 回答