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

List 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()方法时减少内存使用/对象创建

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

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

代替。

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

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

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

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

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

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

这是提及新的String Deduplicationfunction的地方。 这不会改变字符串实例,即这些对象的标识,因为这会改变程序的语义,但更改相同的字符串以使用相同的char[]数组。 由于这些字符数组是最大的有效负载,这仍然可以节省大量内存,而没有使用intern()的性能缺点。 由于此重复数据删除是由垃圾收集器完成的,因此它仅适用于存活时间足以产生差异的字符串。 此外,这意味着当仍有足够的可用内存时,它不会浪费CPU周期。


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

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

例如

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

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

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

 static List parse(CharSequence input) { List result = new ArrayList<>(); Matcher m = TOKEN_PATTERN.matcher(input); CharBuffer cb = CharBuffer.wrap(input); HashMap 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或垃圾收集器交互。

您可以打开JMC并在特定JVM的MBean服务器内的“内存”选项卡下检查GC,以及它清除了多少。 尽管如此,它还没有固定的保证时间。 您可以在特定JVM上的诊断命令下启动GC。

希望能帮助到你。