运行时常量池和字符串常量池的关系是什么?

#Java#面试八股

运行时常量池和字符串常量池的关系是什么?

结论先行:**字符串常量池(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」三层画清,再看 == 的比较结果就不会再靠猜。

笔记加载中…