您的示例有点奇怪,因为它创建了 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 或垃圾收集器进行交互。