调用 intern() 方法后,内存中的新 String() 对象何时会被清除

Gok*_*mar 5 java memory garbage-collection memory-management heap-memory

List<String> list = new ArrayList<>();
for (int i = 0; i < 1000; i++)
{
    StringBuilder sb = new StringBuilder();
    String string = sb.toString();
    string = string.intern()
    list.add(string);
}
Run Code Online (Sandbox Code Playgroud)

在上面的示例中,调用 string.intern() 方法后,什么时候会清除堆中创建的 1000 个对象(sb.toString)?


编辑 1:如果不能保证可以清除这些对象。假设 GC 还没有运行,使用 string.intern() 本身是否已经过时了?(在内存使用方面?)

在使用 intern() 方法时,有什么方法可以减少内存使用/对象创建

Hol*_*ger 5

你的例子有点奇怪,因为它创建了 1000 个空字符串。如果你想得到这样一个消耗最少内存的列表,你应该使用

\n\n
List<String> list = Collections.nCopies(1000, "");\n
Run Code Online (Sandbox Code Playgroud)\n\n

反而。

\n\n

如果我们假设正在发生更复杂的事情,而不是在每次迭代中创建相同的字符串,那么调用intern(). 会发生什么取决于implement\xc2\xadtation。但是,当调用intern()不在池中的字符串时,在最好的情况下它只会被添加到池中,但在最坏的情况下,将创建另一个副本并将其添加到池中。

\n\n

此时,我们还没有节省任何资金,但可能会产生额外的垃圾。

\n\n

如果某处有重复项,此时实习只能节省一些内存。这意味着您首先构造重复的字符串,然后通过其查找其规范实例intern(),因此在内存中保留重复的字符串直到垃圾收集是不可避免的。但这\xe2\x80\x99s并不是实习的真正问题:

\n\n
    \n
  • 在较旧的 JVM 中,对 interned 字符串进行了特殊处理,这可能会导致垃圾收集性能变差,甚至耗尽资源(即固定大小的 \xe2\x80\x9cPermGen\xe2\x80\x9d 空间)。
  • \n
  • 在 HotSpot 中,保存临时字符串的字符串池是一个固定大小的哈希表,会产生哈希冲突,因此,当引用的字符串多于表大小时,性能会很差。
    \n在 Java\xc2\xa07、update\xc2\xa040 之前,默认大小约为 1,000,甚至不足以容纳任何不平凡的应用程序的所有字符串常量而不会发生哈希冲突,更不用说手动添加的字符串了。更高版本使用大约 60,000 的默认大小,这更好,但仍然是固定大小,这会阻止您添加任意数量的字符串
  • \n
  • 字符串池必须遵守语言规范规定的线程间语义(就像它用于字符串文字一样),因此,需要执行线程安全更新,这可能会降低性能
  • \n
\n\n

请记住,即使在没有重复项(即没有节省空间)的情况下,您也要为上述缺点付出代价。此外,获取的对规范字符串的引用必须比用于查找它的临时对象具有更长的生命周期,才能对内存消耗产生积极影响。

\n\n

后者触及了你的字面问题。当垃圾收集器下次运行时,即实际需要内存时,将回收临时实例。无需担心何时会发生这种情况,但是,是的,到目前为止,获取规范引用没有任何积极效果,不仅因为到目前为止内存还没有\xe2\x80\x99 被重用,而且,因为在那之前实际上并不需要内存。

\n\n

这里要提到新的字符串重复数据删除功能。这不会更改字符串实例,即这些对象的标识,因为这会更改程序的语义,但会更改相同的字符串以使用相同的char[]数组。由于这些字符数组是最大的有效负载,因此这仍然可以节省大量内存,而不会出现使用intern(). 由于重复数据删除是由垃圾收集器完成的,因此它只会应用于存活时间足够长以产生影响的字符串。此外,这意味着当仍然有足够的可用内存时,它不会浪费 CPU 周期。

\n\n
\n\n

然而,在某些情况下,手动规范化可能是合理的。想象一下,我们\xe2\x80\x99正在解析源代码文件或XML文件,或者从外部源(Reader或数据库)导入字符串,默认情况下不会发生这种规范化,但有一定可能性会发生重复。如果我们计划将数据保留更长时间以供进一步处理,我们可能希望摆脱重复的字符串实例。

\n\n

在这种情况下,最好的方法之一是使用本地映射,不受线程同步的影响,在处理后将其删除,以避免保留引用的时间超过必要的时间,而无需与垃圾收集器进行特殊交互。这意味着不同数据源中相同字符串的出现不会被规范化(但仍然受到 JVM\xe2\x80\x99s字符串重复数据删除的影响),但它\xe2\x80\x99s 是一个合理的权衡。通过使用普通的可调整大小HashMap,我们也不会遇到固定intern表的问题。

\n\n

例如

\n\n
static List<String> parse(CharSequence input) {\n    List<String> result = new ArrayList<>();\n\n    Matcher m = TOKEN_PATTERN.matcher(input);\n    CharBuffer cb = CharBuffer.wrap(input);\n    HashMap<CharSequence,String> cache = new HashMap<>();\n    while(m.find()) {\n        result.add(\n            cache.computeIfAbsent(cb.subSequence(m.start(), m.end()), Object::toString));\n    }\n    return result;\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

请注意此处的使用CharBuffer:它包装输入序列,其方法返回另一个具有不同开始和结束索引的包装器,为我们subSequence实现正确的equals和方法,并且仅当键之前不存在于映射中时才会调用该方法。因此,与 using 不同,不会为已经遇到的字符串创建实例,从而节省了最昂贵的部分,即字符数组的复制。hashCodeHashMapcomputeIfAbsenttoStringintern()String

\n\n

如果重复的可能性非常高,我们甚至可以保存包装器实例的创建:

\n\n
static List<String> parse(CharSequence input) {\n    List<String> result = new ArrayList<>();\n\n    Matcher m = TOKEN_PATTERN.matcher(input);\n    CharBuffer cb = CharBuffer.wrap(input);\n    HashMap<CharSequence,String> cache = new HashMap<>();\n    while(m.find()) {\n        cb.limit(m.end()).position(m.start());\n        String s = cache.get(cb);\n        if(s == null) {\n            s = cb.toString();\n            cache.put(CharBuffer.wrap(s), s);\n        }\n        result.add(s);\n    }\n    return result;\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

这只会为每个唯一字符串创建一个包装器,但在放置时还必须为每个唯一字符串执行一次额外的哈希查找。由于创建包装器的成本非常低,因此您确实需要大量的重复字符串,即与总数相比的少量唯一字符串,才能从这种权衡中获益。

\n\n

如前所述,这些方法非常有效,因为它们使用纯粹的本地缓存,之后就会被删除。这样,我们就不必处​​理线程安全问题,也不必以特殊方式与 JVM 或垃圾收集器交互。

\n


小智 1

您可以打开 JMC 并在特定 JVM 的 MBean Server 内的“内存”选项卡下检查 GC 的执行时间以及清除的数量。不过,对于何时调用它并没有固定的保证。您可以在特定 JVM 上的诊断命令下启动 GC。

希望能帮助到你。