运行时常量池是类加载时将.class文件静态常量池复制到方法区(jdk 8+为元空间)的可动态修改副本,存储字面量和符号引用,并在链接阶段将符号引用解析为直接引用。

方法区里的常量池,本质是类加载过程中从 .class 文件“搬进来”的核心数据结构,不是运行时凭空生成的。它分为两个层面:编译期固化在字节码里的静态常量池(Class文件常量池),和类加载后存入方法区(JDK 8+为元空间)的运行时常量池。二者内容一致但阶段不同,解析过程就发生在这两者之间。
常量池在class文件里长什么样?
它位于class文件开头靠前的位置,结构非常规整:
- 前2个字节是 constant_pool_count,表示常量池项总数加1(比如值为25,说明实际有24个条目);
- 紧接着是 constant_pool 数组,长度为
constant_pool_count - 1,索引从1开始(0不使用); - 每个数组元素是一个
cp_info结构,以1字节的 tag 开头,标识类型(如CONSTANT_Utf8_info、CONSTANT_Class_info、CONSTANT_Methodref_info等); - 后续字节按 tag 类型展开:UTF-8 字符串存字节长度+内容,方法引用存类索引+名称与描述符索引,整数/浮点数直接存4字节原始值。
常量池里到底存了啥?
两类内容并存,缺一不可:
-
字面量(Literal):编译期确定的固定值,比如字符串
"abc"、整数123、浮点数3.14f、null,还有被final修饰且编译期可确定的常量; -
符号引用(Symbolic Reference):不是内存地址,而是“名字+描述”形式的占位符,包括:
– 类或接口的全限定名(如java/lang/Object)
– 字段名及其描述符(如count:I)
– 方法名及其描述符(如toString:()Ljava/lang/String;)。
JVM是怎么解析常量池的?
解析发生在类加载的链接阶段,核心是把符号引用转成直接引用:
- 读取
constant_pool_count,预分配运行时常量池空间; - 逐个读取
cp_info,根据 tag 解析出对应内容,复制进方法区的运行时常量池; - 遇到符号引用(如方法引用),先检查对应类是否已加载;未加载则触发类加载,再定位到具体方法表项;
- 成功解析后,原符号引用被替换成指向内存中实际类、字段或方法的指针(即直接引用);
- 若解析失败(如找不到类、方法签名不匹配),抛出
NoClassDefFoundError或NoSuchMethodError。
字符串常量池和它啥关系?
字符串常量池(String Table)是独立存在但与之协同的机制:
- 编译期的字符串字面量(如
"hello")会作为CONSTANT_Utf8_info存进静态常量池; - 类加载时,这些 UTF-8 条目被读入运行时常量池,但不会自动变成堆中的 String 对象;
- 只有首次执行
ldc "hello"字节码指令(如声明String s = "hello";)时,JVM 才会用该 UTF-8 内容去字符串常量池查重、驻留,并返回堆中对象引用; - 所以字符串常量池是运行时常量池的“下游消费者”,两者物理分离(一个在方法区/元空间,一个在堆),逻辑联动。










