字符串常量池通过复用相同字面量避免重复创建对象:字面量自动入池且只查不建,new强制堆中新建,intern()手动入池;jdk7+移至堆内存以支持动态扩容和gc回收,但intern()仍需谨慎使用。

字符串常量池怎么避免重复创建对象
常量池的核心作用是复用——相同内容的字面量字符串只在内存中存一份。比如连续写 String a = "hello"; String b = "hello";,JVM 在类加载阶段就发现第二个 "hello" 已存在,直接让 b 指向池中那个对象,不再分配新空间。
这和 new String("hello") 完全不同:new 强制在堆上新建对象,哪怕常量池里早有 "hello",也照建不误。结果就是两个对象:一个在池里,一个在堆里,== 比较必为 false。
- 字面量(双引号)自动入池,且只查不建(已有就复用)
-
new String(...)总是在堆上新建,与池无关 -
intern()是手动“申请入池”的入口:若池中无此内容,则把当前字符串(或其引用)加入池并返回池中引用
为什么 JDK 7+ 把常量池移到堆里
JDK 6 及以前,常量池在方法区(PermGen),大小固定、难扩容,容易 java.lang.OutOfMemoryError: PermGen space。JDK 7 起移至堆内存,意味着它能随堆一起动态伸缩,也能被 GC 正常回收(前提是池中字符串没被强引用)。
这个改动直接影响 intern() 的行为:在 JDK 7+ 中,new String("abc").intern() 首次调用时,会把堆中那个对象的引用放入池;而 JDK 6 中,它只能把字符串内容拷贝一份到 PermGen,造成额外开销。
- 堆中常量池可被 GC 回收(当池中字符串无外部引用时)
- 避免 PermGen / Metaspace 溢出风险
-
intern()在 JDK 7+ 更轻量,但依然不建议无差别调用
intern() 不是万能的,用错反而拖慢系统
很多人看到“节省内存”就以为 intern() 越多越好,其实它有明确适用边界。它的代价是:每次调用都要查表(哈希查找)、可能触发池扩容、且入池后的字符串在 JVM 生命周期内很难被 GC(除非池本身被清理)。
典型误用场景:
- 对用户输入、日志消息、UUID、时间戳等几乎不重复的字符串调用
intern() - 在循环里对动态拼接结果(如
"user_" + id)反复intern() - 未限制池大小,导致元空间/堆内存持续增长(尤其在长期运行的微服务中)
真正适合 intern() 的,是那些内容固定、重复率高、生命周期长的字符串,比如 HTTP 方法名("GET"、"POST")、状态码("ACTIVE"、"PENDING")、配置项 key。
String 不可变性是常量池安全的前提
如果 String 是可变的,常量池立刻失效。想象一下:String a = "abc"; String b = "abc"; 共享同一个池中对象,此时某段代码通过反射修改了 a 的内部 value 数组,那 b 的值也会跟着变——这是灾难性的。
不可变性靠三重保障:
-
String类是final,无法被继承覆盖行为 - 内部存储字段(
value)是final byte[](JDK 9+)或final char[](JDK 8-),引用不可重赋 - 所有“修改”方法(
concat、replace、substring等)都返回新对象,不碰原对象
没有这三点,JVM 就不敢放心地让成百上千个变量共享同一个字符串实例——优化就无从谈起。











