JDK 6 中 String.intern() 将字符串复制到永久代常量池,返回新地址;JDK 7+ 则在堆中常量池查找并直接返回已有对象引用,基于 equals() 匹配,复用更高效。

String.intern() 在 JDK 6 和 JDK 7+ 的行为根本不同
不是“用法变了”,是底层实现逻辑彻底重排:JDK 6 把 intern() 返回的字符串放进永久代(PermGen)的字符串常量池,而 JDK 7 起挪到了堆内存(Heap)里的字符串常量池。这意味着,intern() 是否能复用已存在的堆对象,直接取决于你用的是哪个版本。
常见错误现象:new String("abc").intern() == new String("abc") 在 JDK 6 是 false,在 JDK 7+ 可能是 true(前提是 "abc" 字面量已加载过)。
- JDK 6:常量池独立于堆,
intern()总是复制一份到永久代,返回新地址 - JDK 7+:常量池在堆中,
intern()先查池中有没有相同内容的String实例;有就直接返回那个堆对象引用 - 注意:这个“查”是基于
equals(),不是引用比较,所以内容相同即可匹配
什么时候调用 intern() 才真能省内存?
它不自动优化所有字符串,只对明确走 new String(...) 构造、且内容高度重复的场景有效。日常拼接、substring()(JDK 7u6 之后已改用堆拷贝)、JSON 解析出的字符串,基本都不适合盲目 intern()。
使用场景举例:读取大量配置项或枚举值(如状态码字符串 "SUCCESS"、"FAILED"),它们反复出现但来源是 new String(bytes, charset)。
立即学习“Java免费学习笔记(深入)”;
- 必须确保字符串内容稳定、重复率高,否则
intern()反而增加 GC 压力(常量池对象长期存活) - 不要对动态生成的、带时间戳/UUID 的字符串调用
intern(),100% 白费且拖慢性能 - JDK 8u20 后默认开启
-XX:+UseG1GC,但字符串常量池仍受 G1 的 humongous object 分配影响,大字符串intern()可能触发额外 GC
判断 intern() 是否生效,别只看 ==,要结合 -XX:+PrintStringTableStatistics
光靠 == 比较容易误判,尤其当字面量已经存在时,JVM 会提前把它们塞进常量池——这时你甚至没显式调用 intern(),结果也是 true。
真正要看的是常量池里到底存了几个对象、是否发生了复用。最直接的方式是加 JVM 参数观察:
-XX:+PrintStringTableStatistics -XX:+UnlockDiagnosticVMOptions
启动后会打印类似:
StringTable size = 1009, entries = 42, buckets = 15, collisions = 3
-
entries表示当前池中唯一字符串个数,增长缓慢说明复用成功 -
collisions高说明哈希冲突多,可能影响intern()性能(但一般不用管) - 别信 IDE 调试器里显示的 “value” 字段,它可能被优化显示,实际引用关系得看
==或 jmap + jhat
HotSpot 源码里 intern() 的关键分支就在 java_lang_String::intern
如果你翻 OpenJDK 8 的 src/hotspot/share/classfile/javaClasses.cpp,会发现 java_lang_String::intern 方法里有个硬编码判断:if (EnableStringDeduplication) 是另一套机制(G1 的字符串去重),和 intern() 完全无关。别混淆。
真正逻辑是:先查 StringTable::lookup,查不到就调用 StringTable::intern 插入新条目并返回——这个插入动作在 JDK 7+ 就是直接存堆对象指针,不再 copy。
- 所以
intern()不是“把字符串放常量池”,而是“把字符串的引用登记进一张全局哈希表” - 这张表本身在堆上(JDK 7+),但它存的值是其他
String对象的引用,不是副本 - 这也是为什么
intern()后的对象能参与 GC:只要没其他强引用,它和普通堆对象一样可回收










