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

Posted

技术标签:

【中文标题】调用 intern() 方法后,内存中的 new String() 对象何时被清除【英文标题】:When will the new String() object in memory gets cleared after invoking intern() method 【发布时间】:2018-06-15 06:44:33 【问题描述】:
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);

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


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

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

【问题讨论】:

它们可以在完整的 GC 上被清除,但不能保证何时会发生。 AFAIK,在第一个循环中创建的对象将不会被 GC,因为它已被实习。每当 GC 运行时,其他的就会消失。 每个可能在由于语句string=string.inter() 被执行而变得未引用后的任何时间都被垃圾收集。可能是立即,可能是半秒后可能永远不会。 lmgtfy: java-performance.info/string-intern-in-java-6-7-8 Is it good practice to use java.lang.String.intern()?的可能重复 【参考方案1】:

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

List<String> list = Collections.nCopies(1000, "");

改为。

如果我们假设发生了一些更复杂的事情,而不是在每次迭代中创建相同的字符串,那么调用intern() 没有任何好处。会发生什么,取决于实现。但是当在一个不在池中的字符串上调用intern()时,最好的情况只是将它添加到池中,但在最坏的情况下,会制作另一个副本并添加到池中。

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

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

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

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

后者触及你的字面问题。当垃圾收集器下次运行时,临时实例会被回收,这将是实际需要内存的时候。无需担心何时会发生这种情况,但是,是的,到目前为止,获取规范引用没有任何积极影响,不仅因为到那时内存还没有被重用,而且,因为直到那时才真正需要内存。

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


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

在这种情况下,最好的方法之一是使用 local 映射,不受线程同步的影响,在进程之后将其删除,以避免保持引用超过必要的时间,而不必使用与垃圾收集器的特殊交互。这意味着在不同数据源中出现的相同字符串未规范化(但仍受 JVM 的 String Deduplication 约束),但这是一个合理的权衡。通过使用普通的可调整大小的HashMap,我们也没有固定intern 表的问题。

例如

static List<String> parse(CharSequence input) 
    List<String> result = new ArrayList<>();

    Matcher m = TOKEN_PATTERN.matcher(input);
    CharBuffer cb = CharBuffer.wrap(input);
    HashMap<CharSequence,String> cache = new HashMap<>();
    while(m.find()) 
        result.add(
            cache.computeIfAbsent(cb.subSequence(m.start(), m.end()), Object::toString));
    
    return result;

注意这里CharBuffer的使用:它包装输入序列及其subSequence方法返回另一个具有不同开始和结束索引的包装器,实现正确的equals和@987654335我们的HashMap 的@ 方法和computeIfAbsent 将只调用toString 方法,如果键之前不存在于映射中。因此,与使用 intern() 不同,不会为已经遇到的字符串创建 String 实例,从而节省了其中最昂贵的方面,即字符数组的复制。

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

static List<String> parse(CharSequence input) 
    List<String> result = new ArrayList<>();

    Matcher m = TOKEN_PATTERN.matcher(input);
    CharBuffer cb = CharBuffer.wrap(input);
    HashMap<CharSequence,String> cache = new HashMap<>();
    while(m.find()) 
        cb.limit(m.end()).position(m.start());
        String s = cache.get(cb);
        if(s == null) 
            s = cb.toString();
            cache.put(CharBuffer.wrap(s), s);
        
        result.add(s);
    
    return result;

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

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

【讨论】:

当你说 but in the worst case... 时,你的意思是像 String s = new String("abc"); s.intern()?。我还只是在这个答案中的那部分,所以如果你不介意我可能会再问一些......这很有趣,以至于在这个明显广为人知的功能中,即使是 SO 也充满了垃圾答案 @Eugene:当您执行new String("abc").intern() 时,在调用intern() 之前您已经有两个字符串实例,并且不会有第三个,因为"abc" 已经是规范字符串。但是过去有intern() 的实现,它总是在向池中添加字符串时创建一个新字符串。这可能与 PermGen 策略有关,也可能与 offsetlength 的子字符串引用更大的 char[] 数组有关,该数组不应被池引用。总而言之,这只是取决于实现是否在这一点上进行复制【参考方案2】:

您可以打开 JMC 并在特定 JVM 的 MBean Server 内的 Memory 选项卡下检查 GC 何时执行以及清除了多少。尽管如此,它被调用的时间并没有固定的保证。您可以在特定 JVM 上的诊断命令下启动 GC。

希望对你有帮助。

【讨论】:

以上是关于调用 intern() 方法后,内存中的 new String() 对象何时被清除的主要内容,如果未能解决你的问题,请参考以下文章

聊聊Java String.intern 背后你不知道的知识

String中intern方法的作用

对String.intern()的理解

常量池之字符串常量池String.intern()

String类中intern方法的原理分析

在静态方法中调用 new Runnable() 是不是可以避免内存泄漏?