【问题标题】:Retained heap size of a string in javajava中字符串的保留堆大小
【发布时间】:2012-01-15 16:57:37
【问题描述】:

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

我了解字符串的实际内容包含在内部 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 的内存问题还是仅仅是分析器显示信息的方式?

这个问题让我们在尝试追踪应用程序中的大量内存使用情况时非常头疼。

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

感谢您的帮助

【问题讨论】:

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


    【解决方案1】:

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

    这里有一个支配树中的传递引用

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

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

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

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

    【讨论】:

    • 我将下载jprofiler的评估,看看是否更有意义。不过谢谢你的回答。它看起来更有意义......
    • 不幸的是,我发现 jprofiler 很难使用。我没有时间学习如何充分发挥它的潜力,所以我只会相信你的话:) 谢谢你的帮助
    • 作为感谢,您可以接受我的回答 :-) 让我向您保证,JProfiler 一点也不难使用。对于上面的示例,您只需拍摄堆快照,选择保存数组的类并激活“最大对象”视图。
    • 我接受了您的回答,因为它确实在帮助我理解发生了什么方面大有帮助。不过,我仍然对 jprofiler 持怀疑态度 ;)
    【解决方案2】:

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

    【讨论】:

    • 我同意...分析器可能正在计算字符串内部数组两次。
    • 我倾向于同意,但是这个问题似乎会导致完全 gc 在可能没有必要的情况下发生 - 换句话说 - 甚至 java 也这样认为
    • 所以你是说 Java 对使用了多少堆空间和有多少空闲空间感到困惑(并且计算同一个对象两次)?这似乎不太可能......
    • 我同意。我认为 java 确切地知道它在做什么——它只是以一种令人困惑的方式来传达它!
    【解决方案3】:

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

    【讨论】:

    • 我认为他说的是两个数组具有完全相同的 String 实例。
    • @Thilo - 我正在接受 “在两个数组中,字符串都引用同一个字符数组。” 如果不将字符串进行实习,很难确保这一点。跨度>
    • 实际上确保这一点是微不足道的。 String s2 = s1.substring(0) 你是对的, new String(char[]) 构造函数将复制 char 数组。然而,new String(String) 构造函数在 IBM JVM 上的行为与在 Sun JVM 上的行为不同。
    • 我希望字符串引用相同的字符数组。在很多情况下,这似乎是一件好事
    • @slbruce - 我认为您可以获得两个共享相同字符数组的字符串对象,如下所示:String a = new String(chars); String b = a.substring(0);。两个 String 都不会使用 chars 作为 char 数组,但它们之间应该共享相同的 char 数组。
    【解决方案4】:

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

    【讨论】:

    • 我不知道您在哪里找到了子字符串实现,但是在 Oracle/Sun 和 IBM JVM 中,子字符串不会复制数组。
    • 我的代码中有一个错误!子字符串来自必须复制的 StringBuilder。
    • 同意。这绝对不是我看到的行为
    • 有趣的事实:StringBuilder 不必复制。在 IBM JVM 中,如果后备数组不浪费太多空间,StringBuilder.toString() 将使用它自己的后备数组构造一个新字符串,并将共享标志设置为 true。只有对 StringBuilder 的后续更改才会触发数组复制 - 通过检查共享标志。 subString 可以使用相同的机制,但由于某种原因它没有。
    猜你喜欢
    • 1970-01-01
    • 2016-01-13
    • 2015-06-15
    • 2014-07-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-14
    • 1970-01-01
    相关资源
    最近更新 更多