常量池和运行时常量池不是一回事:前者是.class文件中静态的constant pool表,后者是类加载时jvm将其复制到内存(元空间/方法区)后的可动态修改版本,支持string.intern()等操作。

常量池和运行时常量池到底是不是一回事
不是。常量池是编译产物,运行时常量池是它在 JVM 里的“活体副本”。javac 编译后生成的 .class 文件里那个 Constant pool 表,就是常量池——它静止地躺在磁盘上,只读、不可改;而运行时常量池是类加载时,JVM 把这张表整个拷贝进内存(方法区 / 元空间)后的结果,支持动态写入,比如 String.intern() 就是往这里塞新字符串。
常见错误现象:javap -v MyClass.class 看到一堆 #1 = String #2 这样的编号,就以为这是“运行时”的东西——其实这只是静态快照,还没进 JVM。
- 使用场景:调试类加载问题、分析符号引用解析失败(如
NoClassDefFoundError常和符号引用未正确解析有关) - 参数差异:
javap -v查的是常量池;jmap -histo或jcmd <pid> VM.native_memory</pid>才能间接反映运行时常量池占用 - 性能影响:运行时常量池过大(比如大量
intern()非必要字符串)会拖慢类加载,甚至引发元空间 OOM(java.lang.OutOfMemoryError: Metaspace)
字符串常量池从永久代搬到堆里,到底改了什么
JDK 7 是分水岭:字符串常量池(StringTable)被从永久代(PermGen)挪到了堆(Heap),不是“运行时常量池搬家”,而是字符串池单独拆出来、换地方了。
关键变化在于 GC 行为和内存归属:
- JDK 6 及之前:字符串常量池在永久代,GC 不轻易回收,
intern()大量重复字符串容易撑爆 PermGen,报java.lang.OutOfMemoryError: PermGen space - JDK 7:字符串池移到堆中,受主流 GC(如 G1、CMS)管理,可被正常回收——但前提是没被其他对象强引用住
- JDK 8+:永久代彻底移除,元空间(Metaspace)只存类元数据;字符串池仍在堆里,和普通对象一样参与 GC
容易踩的坑:new String("abc").intern() 在 JDK 7/8 中确实会把字面量 "abc" 放进堆里的字符串池,但如果堆内存紧张,这个字符串仍可能被 GC 掉(尤其没其他引用时);而很多人误以为 intern() 后就“永久驻留”了。
为什么 s == "abc" 有时 true,有时 false
本质是看左边变量指向的是字符串池里的对象,还是堆里新创建的对象。Java 对字符串字面量自动做池化,但对运行期拼接或 new String() 创建的则不会。
典型例子:
String s1 = "abc"; // 池中,s1 → 字符串池
String s2 = new String("abc"); // 堆中,s2 → 堆对象
String s3 = s2.intern(); // 返回池中引用,s3 → 字符串池
System.out.println(s1 == s2); // false
System.out.println(s1 == s3); // true
- 使用场景:判断字符串是否为编译期已知字面量(如配置 key、协议标识),用
==比.equals()快,但必须确保两边都来自池 - 容易踩的坑:
"ab" + "c"编译期优化成"abc",所以s1 == ("ab"+"c")是 true;但"ab" + new String("c")是运行期拼接,结果在堆,==就是 false - 兼容性注意:JDK 9+ 的字符串内部实现从
char[]改为byte[],但字符串池行为不变,不影响==判断逻辑
怎么验证某个字符串真进了字符串常量池
不能光靠 ==,得结合 intern() 行为 + 内存工具交叉验证。最直接的办法是让字符串“被迫”进池,再比引用。
实操建议:
- 用
String s = new String("hello").intern(),然后与字面量"hello"做==,true 就说明它已在池中 - 更底层验证:启动 JVM 加
-XX:+PrintStringTableStatistics(JDK 8u60+),程序退出时会打印字符串池大小、冲突数等,确认是否增长 - 避免误判:不要用
System.identityHashCode()或toString()判断,它们不反映内存位置;==是唯一可靠的引用相等判断方式
真正复杂的地方在于:字符串池不是全局独占资源,它是每个 ClassLoader 维护一份(虽然通常共用 Boot ClassLoader 的池),且 intern() 是 native 方法,底层用的是 C++ 实现的哈希表——这意味着并发调用 intern() 有锁竞争,高频调用反而可能成为瓶颈。









