字符串常量池自jdk 7起位于堆内存中,jdk 6及以前在永久代,jdk 8+仍保留在堆中而非元空间;其迁移使字符串可被gc回收,显著降低oom风险并影响intern()的实际行为。

字符串常量池到底在哪?位置变化影响实际行为
字符串常量池不是固定在某个“神秘区域”,它的物理位置随JDK版本演进发生过关键迁移:
- JDK 6及之前:放在永久代(PermGen),大小固定、不参与GC,容易因大量
intern()触发OOM - JDK 7开始:移到堆内存中,和普通对象一样受GC管理,可动态扩容,大幅降低内存泄漏风险
- JDK 8及之后:永久代被元空间(Metaspace)取代,但字符串常量池仍保留在堆里——它从来就不属于元空间
这意味着:你在JDK 8+调用intern()后,如果该字符串不再被任何变量引用,它可能被垃圾回收;而JDK 6下一旦进入常量池,基本“永驻”——这点直接影响缓存策略设计。
两种创建方式,内存行为完全不同
写法差异小,底层开销差别大:
-
String s = "hello";→ 只查常量池:存在则复用,不存在则入池并返回引用。全程不产生堆上String实例 -
String s = new String("hello");→ 先确保常量池有"hello"(可能新建),再在堆上new一个String对象,内部value数组指向常量池里的char[](JDK 7+)或byte[](JDK 9+)
所以new String("abc")不一定创建2个对象:若"abc"字面量此前已出现过(比如前面写了String a = "abc"),那这次只在堆新建1个;否则常量池+堆各1个。别凭空记“一定两个”,看上下文。
intern()不是万能钥匙,用错反而拖慢性能
intern()本质是“手动把字符串塞进常量池并返回其引用”,但它有明确适用边界:
- 适合场景:长期存活、内容重复率高、作为key频繁使用的字符串,如HTTP方法名("GET"、"POST")、配置项键名、枚举标识符
- 不适合场景:临时拼接的路径、用户输入、日志消息体、循环内生成的短生命周期字符串——这些调用
intern()会徒增堆→常量池拷贝开销,还可能撑爆元空间(JDK 7/8)或堆(JDK 9+) - 注意细节:
intern()返回的是常量池中的引用,不是原对象;原堆对象仍存在,只是多了一条指向池的链路
验证技巧:用==比引用,.equals()比内容。例如new String("x").intern() == "x"为true,但new String("x") == "x"恒为false。
实战优化建议:少创建、早复用、慎intern
真正提升字符串性能,靠的不是炫技式调用,而是克制与预判:
- 优先使用字面量赋值,避免无意义的
new String(...)——除非你明确需要隔离副本(比如防止外部修改影响内部状态) - 拼接场景用
StringBuilder而非+(尤其循环中),避免中间产生大量废弃String对象 - 对已知有限集合的字符串(如状态码、协议字段),可在启动时批量
intern(),后续直接字面量比较即可 - 监控手段:通过JVM参数
-XX:+PrintStringDeduplicationStatistics(G1 GC)或jstat -gc观察字符串去重效果,而非盲目加intern()
常量池不是银弹,它是JVM为高频重复字符串准备的共享缓存。理解它在哪、怎么进、何时用,才能让String既轻量又可靠。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











