虚拟机常量池是类文件里的静态描述,运行时常量池是它被JVM加载到方法区后的动态版本,二者的本质区别在于“静态”和“动态”。 虚拟机常量池负责保存编译期生成的符号引用和字面量,而运行时常量池在类加载过程中被创建,不仅能容纳前者拷贝过来的内容,还会在运行时动态添加和解析新常量。
虚拟机常量池和运行时常量池的区别,最直观的差异在哪?
存储位置和生命周期完全不一样
- 虚拟机常量池存在于
.class文件中,属于字节码文件结构的一部分,即使JVM不运行,它依然安静地躺在磁盘上。 - 运行时常量池则存放在JVM的方法区中,HotSpot虚拟机从JDK 8开始,方法区的实现变成了元空间。
- 前者的生命周期跟随类文件,只要文件不删,它就一直在;后者的生命周期跟随类加载器,当类被卸载时,运行时常量池才会被回收。
许多人把这两个概念混在一起,是因为字节码文件里的“常量池”字段名就叫 Constant Pool,但你需要记住:一个是在文件里,一个是在内存里。
变化:从符号引用到直接引用
虚拟机常量池里保存的是字面量和符号引用,比如一个方法 hello() 的调用,在编译后只是记录一个符号引用,类似“调用哪个类的哪个方法”,但没有具体的内存地址。
运行时常量池则会在类加载的解析阶段,把部分符号引用替换成真正的直接引用,也就是说,你的代码运行时访问的不再是“名字”,而是具体的JVM内存地址。
行业共识认为,运行时常量池就是虚拟机常量池的“运行时视图”,但这也不是简单的复制,因为它还支持动态添加内容。
从字节码到内存:类加载过程中常量池发生了什么?
加载阶段:只搬运数据,不解析
当JVM遇到一个类时,会通过类加载器读取 .class 文件,常量池里的信息会被完整搬运到运行时常量池中,此时所有引用仍然保持符号形式,并没有被替换成地址。
你可以用 javap -v 命令查看类文件常量池:
javap -v Target.class
命令输出里会看到 #1 = Methodref、#2 = String 这样的条目,这就是虚拟机常量池的原始样貌。
解析阶段:按需把名字变成地址
类加载的链接过程包含验证、准备和解析,解析阶段发生的时机并不完全固定,HotSpot虚拟机允许延迟解析也就是说,只有当某一符号引用被第一次主动使用时,才发生替换。
举个例子:
public class Demo {
public static void main(String[] args) {
System.out.println("hello");
}
}
System.out 最初在常量池里只是一个字段符号引用,程序运行时,JVM会精确地找到 System 类的 out 字段的内存位置,并替换掉原来的符号引用,这一步,就发生在运行时常量池中。
动态性:运行时常量池能“做加法”
和存储在文件里的静态常量池不同,运行时常量池允许代码在运行期间放入新的常量,最典型的操作是调用 String.intern():
String s = new String("a") + new String("b");
s.intern();
intern() 会去运行时常量池中查找是否有相同内容的字符串,如果没有,就把当前字符串存入,这个动作是在程序运行期完成的,虚拟机常量池根本参与不了,正是这种动态特性,让Java可以支持基于 invokedynamic 的灵活语法,比如Lambda表达式的实现。
常量池在JVM哪个区?怎么用命令实际验证?
如果你问一个面试官“虚拟机常量池和运行时常量池有什么区别?”,对方大概率会追问一句:它们在JVM哪个区?
- 虚拟机常量池:不在JVM内存分区里,它在磁盘的
.class文件中。 - 运行时常量池:在方法区,直接JDK 8后的HotSpot来说,就是元空间(Metaspace)。
- 字符串常量池:这是一个容易混淆的概念,它并不是独立的区,而是运行时常量池的“租客”,不过从JDK 7开始,HotSpot把字符串常量池移到了堆中。
你可以通过调整元空间大小来间接观察运行时常量池的占用:
java -XX:MaxMetaspaceSize=64m -XX:MetaspaceSize=32m Demo
如果你写一个循环不断向运行时常量池添加字符串,会看到元空间内存增长,最终抛出 java.lang.OutOfMemoryError: Metaspace。
JDK7前后运行时常量池位置变化,和永久代有什么关系?
在JDK 7及以前,HotSpot使用永久代来实现方法区,运行时常量池自然也在永久代中,JDK 8把永久代整个删除,方法区迁移到了元空间,两者最大的区别是:
- 永久代有固定上限,受
-XX:MaxPermSize控制。 - 元空间使用本地内存,默认情况下最大值仅受物理内存限制。
对于大量使用 String.intern() 或动态生成类的场景,这个变化非常直接影响,因为永久代一旦不足就容易抛出 OutOfMemoryError: PermGen space,而元空间会让你的本机内存先被耗尽。
面试常问:类常量池和运行时常量池谁先加载?有什么坑?
加载顺序的真相
注意,我这里用的是“类常量池”,也就是虚拟机常量池,两者之间不存在“谁先加载”的先后角逐,因为加载操作的主体是JVM。
准确流程是:
- 类加载器读取
.class文件。 - JVM在加载阶段就创建该类对应的运行时常量池。
- 把虚拟机常量池中的内容逐项拷贝进去。
- 解析阶段按需替换或补充。
所以不是“先加载类常量池,再加载运行时常量池”,而是“读取文件中的常量池内容,在内存中实例化为运行时常量池”。
常见误区:final变量一定在常量池里吗?
很多人以为所有被 final 修饰的变量都会进入常量池,其实不准确,编译期常量(基本类型和字符串字面量合成)会写入虚拟机常量池,但 new 出来的对象引用,即使赋给 final 引用变量,也不会出现在常量池中,只会在运行时涉及对象分配。
再提一个坑:不要在不同类加载器加载的类之间,用 比较来自运行时常量池的字符串,每个类加载器作用域下的运行时常量池是相互隔离的,同名字符串可能存储在不同位置。
运行时常量池和字符串常量池是一回事吗?如何区分?
这个疑问几乎人人都会撞上,简单说,它们有交集,但不是同一个东西。
- 运行时常量池包含类文件常量池的全部条目,还包含动态添加的常量。
- 字符串常量池专门存放字符串字面量和通过
intern()显式添加的字符串。 - 在HotSpot中,字符串常量池是运行时常量池的一部分,但从JDK 7开始,它被拆分到了堆区。
你可以把关系理解成一个“库房里的特殊货架”:库房是运行时常量池,但货架放到了另一个房间(堆),方法区里的运行时常量池仍然负责存储其他类型常量,比如数字、类引用、方法引用。
对照表:三类常量池的区别
| 维度 | 虚拟机常量池 | 运行时常量池 | 字符串常量池 |
|---|---|---|---|
| 存储位置 | .class 文件 |
方法区(元空间) | 堆 |
| 创建时机 | 编译期生成 | 类加载时创建 | 类加载和运行期均可 |
| 可修改性 | 不可修改 | 可动态添加和解析 | 可动态添加 |
| 内存归属 | 无 | JVM内存 | JVM内存 |
| 生命周期 | 随文件 | 随类卸载 | 随GC回收 |
Q&A:虚拟机常量池和运行时常量池有什么区别?现场答疑
问题1:运行时常量池是虚拟机常量池的简单拷贝吗?
不是,它拷贝了类文件常量池的原始条目,但之后会出现两个变化:符号引用被解析成直接引用,程序运行时还能往里添加新常量,所以运行时常量池的内容始终大于等于初始拷贝值。
问题2:每个类都有单独的运行时常量池吗?
是的,每个类或接口在加载成功后,都会在方法区中拥有一块属于自己的运行时常量池,互不干扰,这也解释了为什么不同类中同名的字符串常量,在物理存储上可能是两份。
问题3:运行时常量池溢出时会不会报OutOfMemoryError?
会,因为运行时常量池属于方法区,当动态添加的常量数量超过方法区可用空间时,JVM会抛出 java.lang.OutOfMemoryError: Metaspace(JDK 8及以上版本),或 java.lang.OutOfMemoryError: PermGen space(JDK 7及以前版本)。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/771984.html


