运行时常量池是方法区/元空间中存储类符号引用解析结果的结构,字符串常量池是堆中专用于字符串驻留的哈希表;二者物理位置、作用和调优方式均不同。

运行时常量池是方法区/元空间里的通用常量表,字符串常量池是堆里专管String引用的哈希表
运行时常量池(Runtime Constant Pool)是类加载时从.class文件的静态常量池解析而来的内存结构,属于方法区(JDK 8+ 是元空间)的一部分;它存储所有类型的符号引用解析结果——比如CONSTANT_Class指向的Klass*指针、CONSTANT_Methodref解析后的入口地址,以及CONSTANT_String条目对应的String对象引用。
字符串常量池(String Table)则是独立存在的、专门用于驻留(intern)字符串的哈希表,JDK 7+ 起固定位于**堆内存**中,底层是固定大小的HashTable(默认容量 60013),只存String对象的引用,不存对象本身。
关键区别在于:前者是类元数据的运行时视图,后者是字符串去重的缓存设施;二者在逻辑上有关联(CONSTANT_String解析时会查String Table),但物理位置、生命周期、扩容机制完全不同。
String s = "abc" 触发的是字符串常量池查找,不是运行时常量池分配
编译期,"abc"被写入.class的CONSTANT_Utf8和CONSTANT_String条目;类加载解析阶段,JVM 检查该CONSTANT_String是否已在String Table中:
- 若已存在(比如之前有
String t = "abc"),直接返回其引用,不新建对象 - 若不存在,则在堆中创建
String对象,并将该引用放入String Table - 整个过程不向运行时常量池“写入”新条目,只是读取并复用已有
CONSTANT_String的解析结果
new String("abc")则绕过String Table查找,强制在堆新建对象,即使"abc"已在池中。
intern() 方法的行为严重依赖 JDK 版本,本质是往字符串常量池插入引用
String.intern() 的语义在 JDK 6 和 JDK 7+ 有根本差异:
- JDK 6:若池中无对应字符串,会把当前
String对象**复制一份**到永久代(当时字符串池所在),再返回新副本的引用 - JDK 7+:若池中无对应字符串,直接把当前
String对象的**引用**存入堆中的String Table,返回原对象引用(不复制) - 无论哪个版本,只要池中已存在相同内容的字符串,
intern()都返回池中那个引用
这意味着:new String("abc").intern() == "abc" 在 JDK 7+ 为 true,但在 JDK 6 是 false(因为复制后对象地址变了)。
运行时常量池溢出 ≠ 字符串常量池溢出,调优参数完全不同
运行时常量池属于元空间(JDK 8+)或永久代(JDK 7),其大小由-XX:MaxMetaspaceSize(或-XX:MaxPermSize)控制;而字符串常量池在堆中,其底层哈希表容量由-XX:StringTableSize指定(默认 60013),且表中每个槽位只存一个引用,不占额外堆空间。
容易踩的坑:
- 误以为增大
-Xmx就能缓解字符串池哈希冲突——其实冲突取决于StringTableSize和字符串内容分布,跟堆总大小无关 - 用
jstat -gc看不到字符串池使用情况,得用jmap -histo或jdk.jfr事件跟踪String#intern调用频次 - 大量动态生成字符串并反复调用
intern(),可能使String Table长期持有对象引用,阻碍 GC,尤其当字符串内容高度随机时
字符串常量池的“不可见性”最强——它不参与GC Roots枚举,也不暴露在标准内存分析工具里,排查泄漏时最容易被忽略。


















