运行时常量池和字符串常量池的关系是什么?
结论先行:**字符串常量池(string pool,HotSpot 里叫 StringTable)**是运行时常量池中字符串字面量的「落地缓存」——类文件里的 CONSTANT_String 项在解析后指向堆上的 String 实例,StringTable 就是这个实例的哈希表索引。运行时常量池是每个类一份、位于元空间的完整结构,存的远不止字符串,还有符号引用、数值字面量、方法句柄等。一句话:字符串池是运行时常量池的一个子集在堆上的投影,JDK 7 起它的物理位置从永久代搬到了堆。
一、三者关系
Hello.class
└── constant_pool: #4 = String → #5 = Utf8 "hello"
│ 类加载
▼
运行时常量池(元空间,每个类一份)
├── #4 解析为 → 指向堆中 String 对象的引用
├── 符号引用(Class/Methodref)→ 解析后变直接引用
└── 数值字面量、方法句柄、动态调用点
│ 字符串字面量落地
▼
字符串常量池 StringTable(堆,全局唯一)
└── "hello" → 同一个 String 实例(可被所有类共享)
| 维度 | 运行时常量池 | 字符串常量池 |
|---|---|---|
| 位置 | 元空间(JDK 8+) | 堆(JDK 7+) |
| 归属 | 每个类一份 | 全 JVM 一份(StringTable) |
| 内容 | 字面量、符号引用、直接引用、动态常量 | String 实例的引用表 |
| 共享性 | 类私有 | 全局共享,intern() 操作它 |
| 容量控制 | -XX:MaxMetaspaceSize | -XX:StringTableSize(桶数量) |
二、对象到底创建了几个
public class StringPoolDemo {
public static void main(String[] args) {
String a = "hello"; // 常量池已有则复用;没有则创建 1 个并放入池
String b = "hello"; // 复用同一个实例
String c = new String("hello"); // 堆上新对象(字面量已存在,不新建池内实例)
String d = new String("hel") + "lo"; // 编译期折叠 + 运行期拼接 → 堆上新对象
String e = c.intern(); // 返回池中实例
System.out.println(a == b); // true
System.out.println(a == c); // false
System.out.println(a == e); // true(intern 返回池内实例)
System.out.println(a == d); // false(d 是运行期拼接结果)
}
}
new String("hello") 创建的对象数量取决于字面量是否已在池中:首次是 2 个(池内 1 + 堆上 1),之后是 1 个(只有堆对象)。
三、从字节码看折叠
public class FoldDemo {
static final String PREFIX = "he";
public static void main(String[] args) {
String s = PREFIX + "llo"; // 编译期就能算出来,直接变成 "hello"
System.out.println(s == "hello"); // true
}
}
javap -c -constants FoldDemo.class | sed -n '/main/,/return/p'
# 可见 ldc "hello",而不是运行时 StringBuilder 拼接:
# 0: ldc #7 // String hello
final 修饰的常量字符串会被 javac 折叠进常量池,所以运行时比较是同一实例;而用 StringBuilder 或变量拼接则不会折叠。
四、调优与观测
# 加大 StringTable 桶数量,减少长链表带来的 intern 变慢(大字符串场景)
java -XX:StringTableSize=1000003 -jar app.jar
# 打印字符串池统计(需 debug 版或 JDK 11+ 的 jcmd)
jcmd <pid> VM.stringtable
jcmd <pid> VM.symboltable
# JDK 8 观察永久代/元空间变化(字符串池在堆,不在元空间)
jstat -gcmetacapacity <pid> 1000 5
五、常见追问
| 追问 | 回答要点 |
|---|---|
| 为什么 JDK 7 把字符串池移到堆 | 永久代空间小且回收效率低,字符串数量大易 PermGen space;移到堆后随 Young GC 就能回收 |
intern() 在 JDK 6 与 7+ 有区别吗 | JDK 6 是把字符串复制到永久代并返回副本;JDK 7+ 直接把堆中引用登记进池,不再复制 |
| 字符串池会被回收吗 | 会,池中 String 若无强引用,随堆 GC 一起回收;但 StringTable 本身是全局结构 |
| 一个类能有多少常量池项 | 上限 65535(constant_pool_count 为 u2),超出编译报 constant pool overflow |
常量池和 StringBuilder 有关系吗 | 无直接关系,后者是运行期字符串拼接的普通对象;拼接结果不进池,除非调用 intern() |
小结
字符串池是运行时常量池中字符串字面量在堆上的共享索引,运行时常量池本身是元空间里每个类私有的结构。把「类文件常量池 → 运行时常量池 → StringTable」三层画清,再看 == 的比较结果就不会再靠猜。