什么是 Class 常量池,和运行时常量池区别是什么?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. Class 文件结构理解:是否了解 .class 文件中常量池的作用和存放内容,这是理解类加载机制的基础。

  2. 类加载过程认知:能否说清楚 Class 常量池在类加载过程中如何变为运行时常量池,以及这个转换过程中发生了什么。

  3. 概念辨析能力:能否区分 Class 常量池、运行时常量池、字符串常量池三者的关系和差异,而不是混为一谈。

核心答案

先给结论:

  • Class 常量池:存在于 .class 文件中的 静态 常量表,是编译期生成的字面量和符号引用的集合。它在磁盘上,还没加载到内存。
  • 运行时常量池:Class 常量池被 JVM 加载到内存后的形态,是方法区的一部分。它在运行期可以使用,而且支持 动态写入 新常量。

一句话概括两者的关系:Class 常量池是 "图纸",运行时常量池是按图纸盖出来的 "房子",而且房子还能加盖。

上图展示了从源码到运行时常量池的完整链路。整个过程分为两步:编译期生成 Class 常量池,类加载时将其搬进内存变成运行时常量池。

深度解析

一、Class 常量池里有什么

Class 常量池(也叫静态常量池)是 .class 文件的一部分,可以用 javap -v 命令查看。它主要存两类东西:

1. 字面量(Literal)

就是代码里写死的值:

int a = 100;           // 整型字面量 100
String s = "hello";    // 字符串字面量 "hello"
double pi = 3.14;      // 浮点型字面量 3.14

2. 符号引用(Symbolic Reference)

编译时 Java 还不知道类会被加载到内存的哪个位置,所以用 "符号" 来代替真实的内存地址。包括:

  • 类和接口的全限定名:比如 java/lang/String
  • 字段的名称和描述符:比如 value:[Ljava/lang/String;
  • 方法的名称和描述符:比如 charAt:(I)C

javap -v HelloWorld.class 可以看到类似这样的输出:

Constant Pool:
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = String             #16            // hello
   #3 = Fieldref           #17.#18        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = String             #19            // Hello World
   #5 = Methodref          #20.#21        // java/io/PrintStream.println:(Ljava/lang/String;)V

这就是 Class 常量池的真面目。注意 #1#2 这些编号——Class 常量池里的每一项都有一个索引,其他部分通过索引来引用它。

二、运行时常量池是怎么来的

类加载的时候,JVM 把 Class 常量池的内容加载到方法区,就形成了运行时常量池。但不是简单的 "搬家",中间发生了两个重要变化:

变化一:符号引用 → 直接引用

这是类加载 "解析" 阶段做的事情。Class 常量池里的符号引用(类的名字、方法的名字)会被替换成 直接引用(实际的内存地址或偏移量)。

上图对比了符号引用和直接引用的区别:

  • 符号引用:用字符串描述目标,跟内存布局无关,编译期就能确定。比如 java/lang/String 就是一个符号引用
  • 直接引用:直接指向目标的指针、句柄或偏移量,只有在运行期类加载后才能确定

这个转换不一定在类加载时全部完成,有些 JVM 选择在首次使用时才解析(懒加载)。

变化二:可以动态添加新常量

运行时常量池是 "活的"。除了从 Class 常量池搬过来的内容,运行期间还可以动态写入新的常量。最典型的例子就是 String.intern() 方法:

// intern() 会将字符串加入运行时常量池
String s1 = new StringBuilder("ja").append("va").toString();
String s2 = s1.intern(); // 将 s1 引用的字符串放入运行时常量池

System.out.println(s1 == s2); // true,指向同一个引用

这也是为什么叫 "运行时" 常量池——它不仅在运行时才存在,还支持运行时修改。

三、对比总结

维度 Class 常量池 运行时常量池
存在位置 .class 文件(磁盘) 方法区(内存)
生成时机 编译期(javac 类加载时
内容 字面量 + 符号引用 字面量 + 直接引用 + 动态新增常量
可修改性 静态,不可修改 动态,可运行时添加
生命周期 .class 文件存在 随类的生命周期

四、字符串常量池又是什么?

很多人把运行时常量池和字符串常量池搞混,这里有必要澄清一下。

字符串常量池(String Table / String Pool)是专门为 String 对象设计的一个缓存池,目的是避免重复创建相同内容的字符串对象。

  • JDK 6 及之前:字符串常量池在方法区(永久代)里,和运行时常量池挨着
  • JDK 7:字符串常量池从永久代移到了
  • JDK 8+:仍然在堆中

上图说明了三者的关系。关键点:字符串常量池和运行时常量池是两个独立的东西。字符串字面量在 Class 常量池中以符号引用存在,类加载后会在堆中创建对应的 String 对象,并在字符串常量池中保存引用。

面试高频追问

  1. 追问:String.intern() 方法的作用是什么?

    intern() 会检查字符串常量池中是否已经存在等于当前字符串的对象。如果有,返回池中的引用;如果没有,将当前字符串加入池中并返回引用。JDK 7 之后,如果堆上已经有这个字符串,intern() 只会在池中记录堆上对象的引用,而不会重新拷贝一份。

  2. 追问:JDK 7 为什么要将字符串常量池从永久代移到堆?

    因为永久代空间有限(默认 82MB),字符串常量池放在那里很容易导致 PermGen Space OOM。移到堆之后,空间更大,而且可以被 GC 回收。这也是 JDK 8 彻底移除永久代的一个过渡动作。

  3. 追问:new String("abc") 创建了几个对象?

    最多 2 个。如果字符串常量池中还没有 "abc",会先在池中创建一个字符串对象,然后在堆上再创建一个 String 对象。如果池中已经有了,就只创建堆上的那一个。

常见面试变体

  • "JVM 中有几种常量池?分别是什么?"
  • "运行时常量池和字符串常量池有什么区别?"
  • "Class 文件中的常量池包含哪些内容?"
  • "String.intern() 的原理是什么?"

记忆口诀

三池分清:Class 常量池在文件里(静态图纸),运行时常量池在方法区里(按图纸盖的房子),字符串常量池在堆里(String 专属仓库)。Class 池加载变运行池,符号引用变直接引用,运行池还能动态加。

总结

Class 常量池是 .class 文件中的静态数据,存放字面量和符号引用;运行时常量池是它被加载到内存后的动态版本,存放在方法区,支持解析(符号引用→直接引用)和动态添加。两者是同一份数据在不同阶段的不同形态。面试中如果能顺带把字符串常量池也区分清楚,那就是加分项。