字符串常量池在Java 7及之后版本位于堆内存中,而非方法区或元空间;JDK7将其从永久代移至堆是为了提升回收效率和扩容灵活性,避免PermGen OOM。

字符串常量池到底在堆里还是方法区?
Java 7 及之后版本,字符串常量池明确位于**堆内存(Heap)中**,不是方法区,也不是元空间。这个位置从 JDK 7 开始固化,JDK 8/11/17/21 都延续该设计——哪怕方法区已由永久代(PermGen)改为元空间(Metaspace),字符串常量池也没跟着搬过去,它就“赖”在堆里不走了。
为什么 JDK7 要把字符串常量池从永久代挪到堆?
核心就两条:回收太慢,扩容太难。
- 永久代里字符串几乎不回收:
intern()进来的字符串,得等 Full GC 才可能清理,而 Full GC 触发条件苛刻(老年代+永久代同时告急),实际项目中可能几小时都不来一次 - 永久代默认空间小(如 HotSpot 默认仅 64MB),但业务代码、框架(Spring AOP、MyBatis 动态代理)、JSON 序列化大量调用
String.intern(),极易触发java.lang.OutOfMemoryError: PermGen space - 堆内存大、GC 频繁(Young GC 毫秒级),字符串对象一旦无引用,下次 Young GC 就能顺手清掉;长期存活的还能晋升老年代,和普通对象一视同仁
intern() 行为在 JDK6 和 JDK7+ 有何关键差异?
这是最常踩坑的地方——行为变了,但很多人还按老经验写代码。
- JDK 6:
intern()会把堆上字符串**完整复制一份**到永久代常量池,堆里原对象若无其他引用,就成垃圾;内存翻倍,且复制开销明显 - JDK 7+:
intern()不复制内容,只把堆中已有字符串的**引用存入常量池**(8 字节指针),堆里还是那一份,省空间也省时间 - 副作用:JDK 7+ 中,
"abc".intern() == new String("abc").intern()为true;但"abc".intern() == new String("abc")仍为false(后者是新堆对象)
哪些场景要特别注意字符串常量池的位置变化?
不是所有字符串都自动进池,也不是进了池就安全——真正容易出问题的是手动干预或高吞吐文本处理。
立即学习“Java免费学习笔记(深入)”;
- 读取大量 CSV/日志行后反复调用
line.trim().intern():JDK 7+ 虽不复制,但引用全堆里堆着,若没及时释放引用,照样 OOM(只是从 PermGen 换成 Java Heap) - 使用
StringBuilder拼接后调toString().intern():拼接结果本是临时对象,intern()一加,就变成常驻堆对象,生命周期被意外延长 - 排查
java.lang.OutOfMemoryError: Java heap space时,用 jmap -histo 或 JFR 看堆里 top 类,常会发现java.lang.String占比异常高——这时得查是不是某处滥用intern()或缓存了海量唯一字符串
常量池在堆里,不等于它“免费”;它只是从一块难管的内存,换到了一块好管但容量更透明的内存。管不住引用,照样爆堆。










