类文件常量池是编译期生成的只读二进制数据,运行时常量池是其在jvm元空间中的可变动态副本,负责符号引用解析与intern()映射。

类文件常量池是编译产物,运行时常量池是它在 JVM 内存中的动态副本——前者只读、后者可变,且所有符号引用必须在后者中解析为真实地址才能执行。
怎么看 class 文件里的常量池?用 javap -v 直接翻源码
你写的每个 String s = "abc"、final int MAX = 42、甚至调用的 System.out.println,都会被编译进 .class 文件的常量池里。这不是 JVM 运行时生成的,而是 javac 写死的二进制数据。
- 执行
javap -v MyClass.class,输出最开头的Constant pool:块就是它 - 里面看到的
#1 = String #2、#3 = Methodref #4.#5都是符号引用,没有内存地址,也不能直接调用 - 注意:
new String("abc")中的"abc"会进常量池,但new出的对象本身不会——它在堆上
运行时常量池在哪?为什么 OutOfMemoryError: Metaspace 和它有关
JDK 8+ 中,运行时常量池属于元空间(Metaspace),不是堆,也不是老的永久代(PermGen)。类一加载,JVM 就把 class 文件常量池整个拷进这里,并开始解析。
- 符号引用(比如方法名
"toString":()Ljava/lang/String;)在这里被替换成指向实际方法区中函数入口的指针 -
String.intern()添加的新字符串,也存在这里——所以它能被多个类共享,也能撑爆Metaspace - 如果大量动态生成类(如某些 ORM、热部署框架),或疯狂调用
intern(),就容易触发java.lang.OutOfMemoryError: Metaspace
String.intern() 是怎么桥接两个常量池的?
它本质是把堆上或栈上的字符串对象,“登记”进运行时常量池(也就是元空间里的字符串常量池),并返回那个池子里的引用。这一步不复制内容,只建立映射。
立即学习“Java免费学习笔记(深入)”;
-
String s1 = "abc"→ 编译期字面量,直接进 class 常量池 → 加载后进运行时常量池 -
String s2 = new String("abc").intern()→ 先在堆建对象,再查运行时常量池;若已存在"abc",就返回池中引用(和s1 == s2为true) - ⚠️ JDK 7+ 后,
intern()不再复制字符串内容到永久代/元空间,而是直接记录堆中首次出现的该字符串的引用(前提是没被 GC 掉)
真正容易被忽略的是:类文件常量池和运行时常量池之间没有“实时同步”。改了源码重新编译,class 文件常量池就变了;但老的类已经加载,它的运行时常量池还留在元空间里——除非卸载类(极少发生),否则不会更新。这也是热替换、动态代理等机制要小心处理常量池引用的原因。









