欢迎光临
我们一直在努力

彻底搞懂:常量池、字符串常量池、运行时常量池、静态常量池,一篇讲清

一、引言

在 Java 学习与面试中,常量池永远是绕不开、又极易混淆的核心知识点。很多开发者哪怕有多年开发经验,依然会被这几个概念绕得晕头转向:

  • 日常说的「常量池」到底是一个统称,还是特指某一种?
  • 静态常量池、运行时常量池、字符串常量池,三者在 JVM 中分别存放在哪里?
  • 它们的加载时机、作用范围、生命周期又有哪些本质区别?

这些概念看似只是「名词差异」,实则是理解 JVM 类加载机制、字符串内存布局的关键。本文将从 JVM 底层视角出发,把静态常量池、运行时常量池、字符串常量池这三大核心概念逐一拆解,讲清它们的定义、关联与区别,让你彻底吃透常量池体系,不再被易混概念困扰。

二、常量池

2.1 定义

常量池有两种,一种是字节码文件中的常量池(静态常量池),一种是内存中的常量池(运行时常量池)

这两个概念本质是同一套常量数据在「磁盘」和「内存」两个不同阶段的形态,名字里的「静态 / 运行时」就是对阶段的标注。

维度运行时常量池静态常量池(Class 文件常量池)
存储位置 方法区 / 元空间(内存) .class 字节码文件(磁盘)
生成时机 类加载阶段 编译阶段
内容特性 可动态扩展、包含直接引用 固定不变、仅包含符号引用 + 字面量
生命周期 随类的加载 / 卸载而创建 / 回收 随.class 文件永久存在
核心作用 运行期提供常量的内存访问 编译期记录常量的静态信息

2.2 好处

  • 常量集中存储,减少内存冗余:把类中不变的常量数据(字面量、类 / 方法 / 字段信息)集中管理,避免零散存储导致的内存碎片化;
  • 提前固化 / 解析常量,提升运行效率:编译期固化静态常量池,运行期解析为直接引用,避免运行时重复解析、计算常量;
  • 支撑类加载与执行:是 JVM 加载类、定位方法 / 字段、执行代码的核心数据载体。

三、静态常量池

2.1 定义

静态常量池(也叫 Class 文件常量池)是 Java 源码经javac编译后,固化在.class字节码文件中的二进制常量表,是编译期产物,存储类 / 接口中所有编译期可知的常量数据,包括字符串字面量、基本类型常量、final 常量值等字面量,以及类全限定名、方法名、字段名、方法参数类型等符号引用,它不占用 JVM 内存,仅作为类加载时生成运行时常量池的 “原材料”,且一旦编译完成即只读不可修改。

2.2 储存内容

静态常量池(Class 文件常量池)存储的是编译期就能确定的、类 / 接口相关的所有常量数据

2.2.1 字面量

字面量是程序中直接写死的 “固定值”,编译期就能确定的具体值,是 “数据本身”,是能直接对应到具体数据的常量,也是最容易理解的部分:

  • 字符串字面量:所有双引号包裹的字符串,比如 "hello"、"123"、""(空字符串);
  • 基本类型常量值:编译期可知的基本类型固定值,比如 100、3.14、true、'a';
  • final 常量值:被final修饰且编译期就能确定值的变量,比如 final int MAX = 10 中的10(注意:如果 final 变量值是运行时确定的,不会存入静态常量池)。
  • 2.2.2 符号引用

    这是静态常量池最核心、也是和运行时常量池关联最紧密的部分,不是内存地址,只是 “标识性的名字 / 描述符”,编译期无法确定具体值,只能用符号(名字) 指代的引用,是 “指向数据的标识”,JVM 加载类时会把它解析成内存中的直接引用:

  • 类 / 接口的全限定名:比如 java/lang/String(对应 String 类)、com/test/MyClass(自定义类),用斜杠代替包名的点;
  • 字段的符号引用:包含字段所属的类、字段名、字段类型,比如 MyClass 中的 age:I(I 代表 int 类型)、name:Ljava/lang/String;(L + 全限定名代表引用类型);
  • 方法的符号引用:包含方法所属的类、方法名、方法参数类型、返回值类型,比如 main([Ljava/lang/String;)V([Ljava/lang/String; 代表 String [] 参数,V 代表返回值 void)、add(II)I(两个 int 参数,返回 int);
  • 接口方法的符号引用:专门存储接口中方法的相关标识,格式和普通方法类似。
  • 2.3 字面量,符号引用与变量之间的关系

    • 字符串字面量(如"hello")、基本类型字面量(如10)本身会独立存在于静态常量池(这是 JVM 的规则,和变量无关)
    • 只有static final修饰 + 编译期可确定值(直接赋值字面量,而非方法调用 / 计算)的变量,才会把「变量的符号引用」和「常量池里的字面量」永久绑定(编译期就把变量和字面量的关系写死在常量池里,运行期直接用字面量,无需再找变量赋值。);
    • 其他变量(无 final / 只有 final 但值需运行期计算):

      • 会记录「变量的符号引用」(告诉 JVM 变量的位置 / 类型);
      • 但不会把「变量」和「字面量」绑定(因为值可改 / 编译期不确定);
      • 变量只是运行期临时引用常量池里的字面量,而非「绑定」。

    四、符号引用

    4.1 不使用符号引用的后果

    如果编译时就把代码里的变量 / 方法 / 类直接替换成内存地址,会遇到 3 个致命问题:

    • 跨平台性彻底失效,Java 的口号是 “一次编写,到处运行”,但不同操作系统(Windows/Linux/Mac)、不同 JVM 版本,甚至同一台机器不同次运行,内存布局都完全不同,如果直接写死地址,代码换个环境就直接崩溃 —— 这等于废掉了 Java 的跨平台特性。

    • 编译期根本 “不知道” 运行时的地址 ,Java 代码的编译(javac)和运行(java)是完全分离的两个阶段,编译时,JVM 还没启动,类还没加载,变量 / 方法 / 类都还没分配内存,不知道地址。

    • 动态加载 / 热更新无法实现,Java 支持 “运行时动态加载类”,如果编译时就写死地址,动态加载的类在编译期根本不存在,地址更是无从谈起,甚至同一个类,不同时间加载(比如热更新),地址也可能变,写死地址就等于锁死了代码。

    4.2 编译期未知,运行时绑定

    符号引用本质是 “先记名字,运行时再查地址”,完美解决了直接写地址的所有问题:

    编译期只记 “名字”,不依赖地址

    • 编译时,JVM 只把 “名字” 存入常量池(符号引用),完全不关心它们未来的地址 —— 不管代码跑在哪个平台,“名字” 都是不变的。

    运行时 “解析” 成实际地址

    • 当代码运行到需要使用这些符号时(比如new User()),JVM 会做两件事:
      • 先检查User类是否已加载,没加载就先加载;
      • 把符号引用(User)解析成该类在当前 JVM 中的实际内存地址;
      • 后续再用到User时,会缓存解析后的地址,避免重复解析(保证性能)。

    支持动态链接和多态

    • Java 的多态(比如父类引用指向子类对象)也依赖符号引用:
    • 如果直接写死地址,编译时就会绑定父类的地址,运行时无法动态切换到子类—— 多态特性直接失效。

    4.3 好处

  • 解耦编译和运行:编译时不用依赖第三方类的实际存在,只要名字对就能编译通过,运行时再找实际类;
  • 便于调试和优化:符号引用保留了 “名字”,调试时能看到user.age而不是一堆十六进制地址,JVM 也能基于符号引用做方法内联、常量折叠等优化;
  • 安全性:直接暴露内存地址会有安全风险(比如恶意程序通过地址篡改数据),符号引用相当于一层 “抽象保护”。
  • 五、类的加载过程

    5.1 加载

    JVM 通过类加载器(ClassLoader)完成 3 件事:

  • 通过类的全限定名(如 java.lang.String)找到对应的 .class 文件,读取字节流;
  • 将字节流中的「静态常量池」内容完整复制到方法区的「运行时常量池」;
  • 在内存中生成一个代表该类的 java.lang.Class 对象(存于堆),作为访问该类元数据的入口。
  • 常量池的变化

    • 静态常量池:无变化(仍存在于 .class 文件中,是编译期生成的,不随 JVM 运行而改变。)
    • 运行时常量池:新增该类的所有常量信息,但此时全部是「符号引用」(而非实际内存地址)。

    5.2 验证

    验证阶段是 JVM 的「安全关卡」,确保加载的字节码符合 JVM 规范,不会导致安全问题。其中针对常量池的校验包括:

  • 常量池中的常量类型是否合法(如 Integer 类型是否存的是数字);
  • 符号引用的格式是否正确(如类名是否符合全限定名规则);
  • 引用的类 / 方法 / 字段是否存在(初步校验,不解析地址)。
  • 常量池的变化

    • 运行时常量池内容和结构无任何变化,仅做合法性检查,不修改任何常量信息。

    5.3 准备

    为类的静态变量(包括静态常量)分配内存(存于方法区 / 元空间),并设置「默认初始值」(零值):

    • 基本类型(int/long/boolean 等):默认值为 0/0L/false;
    • 引用类型(String/Object 等):默认值为 null;
    • final 静态常量:分两种情况处理。

    常量池的变化

    这是常量池首次出现「值层面」的变化,核心分两类常量:

    编译期确定的常量(编译期常量)

    • 定义:static final 修饰,且值在编译期就能确定(如 static final int NUM = 100;、static final String MSG = "hello";);
    • 变化:准备阶段直接将「字面量值」存入运行时常量池,而非赋默认值。例如 NUM=100 会直接写入常量池,跳过「默认值 0」的步骤。

    运行期确定的常量(运行时常量)

    • 定义:static final 修饰,但值需运行期计算(如 static final int RAND = new Random().nextInt(););
    • 变化:准备阶段仅为其分配内存,赋默认值(如 RAND=0),值不会写入运行时常量池,需等到初始化阶段才赋值。

    5.4 解析

    JVM 将运行时常量池中的「符号引用」替换为「直接引用」(即内存地址),包括:

  • 类解析:将类的符号引用(如 #1 = Class Demo)替换为 Demo 类 Class 对象的内存地址;
  • 字段解析:将字段符号引用(如 #3 = Fieldref Demo.NUM)替换为 NUM 字段在方法区的内存地址;
  • 方法解析:将方法符号引用(如 #14 = Methodref Demo.main)替换为方法的入口地址(或虚方法表索引)。
    • 延迟解析:解析不是必须在准备阶段后立即执行,JVM 可「懒解析」—— 直到首次使用该符号引用时才解析(如首次调用 Demo.main() 时才解析 main 方法的引用);
    • 异常触发:若解析时发现引用的类 / 方法 / 字段不存在(如引用了一个未加载的类),会抛出 NoClassDefFoundError/NoSuchMethodError 等异常。

    常量池的变化

    • 运行时常量池中的「符号引用」被替换为「直接引用」,从「字符串标识」变为「内存地址」;

    六、运行时常量池

    6.1 定义

    运行时常量池是类加载后,静态常量池在内存中的「运行时版本」 —— 简单说,它是 JVM 把 .class 文件里的「静态常量池」加载到内存后,形成的一个可动态扩展、存储常量信息的内存区域。

    6.2 存储内容

    存储内容说明示例
    静态常量池的副本 从 .class 复制过来的字面量、符号引用(类加载阶段) "hello" 字面量、Test.NUM 符号引用
    解析后的直接引用 符号引用替换成的内存地址(解析阶段) Test.NUM 符号引用 → 0x123456 内存地址
    动态生成的常量 运行期新增的常量(使用阶段) 通过 String.intern() 新增的字符串常量

    6.3 替换为直接引用的问题

    跨平台性问题

    • 直接引用是当前 JVM 运行时计算出来的内存地址,换环境后 JVM 会重新解析,生成新的适配当前环境的地址。→ 跨平台性完全保留。

    编译期不知道地址的问题

    • 直接引用是类加载 / 解析阶段才生成的,此时类已经加载、内存已经分配,地址是确定的。→ 完美解决「编译期未知地址」的问题。

    动态加载 / 热更新问题

    • 动态加载类时,JVM 会在使用时才解析符号引用,生成新类的地址;
    • 热更新后类重新加载,JVM 会重新解析符号引用,绑定新的内存地址;
    • 多态场景下,符号引用会在运行时根据实际对象类型解析为子类地址。→ 动态加载、热更新、多态都能正常工作。

    七、字符串常量池

    7.1 定义

    字符串常量池是 JVM 为优化字符串使用设计的特殊内存区域,核心目的是减少字符串对象的重复创建、节省内存、提高访问效率。是 JVM 中专门存储字符串字面量的引用(而非字符串对象本身,JDK 7 后)的内存区域,本质是一个「哈希表(HashTable)」—— 它会缓存所有使用过的字符串字面量,保证同一个字符串字面量在 JVM 中只有一个唯一的实例。

    7.2 位置

    JDK 版本字符串常量池(StringTable)位置运行时常量池(Runtime Constant Pool)位置核心原因
    JDK 6 及之前 方法区(永久代) 方法区(永久代) 两者都属于方法区的一部分,字符串常量池是运行时常量池的子集
    JDK 7 堆(Heap) 方法区(永久代) 永久代内存有限,字符串常量池易触发 OOM,因此将其移至堆中以利用更大内存;运行时常量池仍留在永久代
    JDK 8 及之后 堆(Heap) 元空间(Metaspace,原方法区逻辑) 永久代被元空间替代,运行时常量池随方法区逻辑移入元空间;字符串常量池继续留在堆中

    八、创建字符串

    8.1 直接使用字面量创建(String s = "hello")

    String s1 = "hello";
    String s2 = "hello";
    System.out.println(s1 == s2); // true

    • JVM 先检查字符串常量池:是否有 "hello" 的引用?
    • 若没有:在堆中创建 "hello" 对象,把对象引用存入字符串常量池;
    • 若有:直接返回常量池中的引用;

    8.2 new 关键字创建(String s = new String("hello"))

    new String("xxx") 每次执行,都会在堆中创建一个全新的字符串对象 —— 不管执行多少次,堆里就会有多少个独立的对象;✅ 但 new 的过程中,JVM 会先去字符串常量池检查 "xxx" 这个字面量:

    • 如果常量池里没有 "xxx" 的引用 → 先在堆创建 "xxx" 对象,把引用存入常量池,再创建 new 对应的新对象(总共 2 个对象);
    • 如果常量池里已有 "xxx" 的引用 → 直接创建 new 对应的新对象。最终:new 出来的这些对象永远不会自动进入字符串常量池,只有 "xxx" 这个字面量会在解析阶段入池(且只入一次)。

    8.3 intern() 方法手动入池

    String s1 = new String("hello");
    String s2 = s1.intern();
    String s3 = "hello";
    System.out.println(s2 == s3); // true
    System.out.println(s1 == s3); // false

    intern() 是 String 类的核心方法,作用是将字符串对象的引用手动存入字符串常量池

    • s1 指向堆中新建的 "hello" 对象(非常量池引用);
    • s1.intern():检查常量池,若已有 "hello" 引用,直接返回该引用;若没有,把 s1 的引用存入常量池并返回常量池的引用;
    • s2 指向常量池中的引用,s3 也指向常量池引用 → s2 == s3 为 true。

    8.4 运行期字符串拼接(a + b 形式)

    String a = "1";
    String b = "2";
    String c = "12";
    System.out.println(a + b == c); // false

    • 编译期行为:
      • a、b 是普通变量,编译器不会将 a + b 优化为字面量 "12";
      • c = "12" 是字面量,直接存入静态常量池,运行时指向字符串常量池的 "12" 引用。
    • a + b 会被编译为 new StringBuilder().append(a).append(b).toString(),在堆中创建一个新的字符串对象,内容为 "12";
      • 这个新对象不会自动进入字符串常量池;

    8.5 编译期常量拼接

    public class test {
    static final String a = "1";
    static final String b = "2";

    public static void main(String[] args) {
    String c = "12";
    System.out.println(a+b == c);//true
    }
    }

    编译器优化为字面量 → 直接指向常量池

    8.6 普通字符串(非变量)拼接

    纯字面量拼接会被编译器直接优化为完整字面量,和直接写 "xxx" 完全一样,会自动入字符串常量池。

    8.9 类加载过程

    所有字符串字面量(如 "hello"),无论变量是否有默认值、是否被 static/final 修饰、是编译期 / 运行期常量,都会且只会在「解析阶段」被加载(创建对象 + 存入引用)到字符串常量池;

    但有两个例外 :

  • new String("hello") 中 new 出来的堆对象,永远不会自动进入字符串常量池(除非手动 intern());
  • 解析阶段支持「懒解析」—— 不是类加载完就立刻入池,而是首次使用该字符串字面量时才触发入池。
  • 赞(0)
    未经允许不得转载:171主机测评 » 彻底搞懂:常量池、字符串常量池、运行时常量池、静态常量池,一篇讲清
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址