侧边栏壁纸
博主头像
牧云

怀璧慎显,博识谨言。

  • 累计撰写 178 篇文章
  • 累计创建 18 个标签
  • 累计收到 8 条评论

目 录CONTENT

文章目录

深入理解 Java 常量池:Class、运行时与字符串常量池的奥秘

秋之牧云
2026-05-08 / 0 评论 / 0 点赞 / 1 阅读 / 0 字

在 Java 面试中,“常量池”是一个高频考点。很多开发者容易混淆 Class 常量池运行时常量池字符串常量池。本文将通过概念解析、内存布局演变及代码实战,彻底厘清这三者的区别与联系,并深入探讨 String.intern() 的核心机制。

一、 三大常量池全景图

1. Class 常量池(静态常量池)

  • 存在形式.class 文件中的二进制数据。
  • 生成时机:编译期。编译器将各种字面量(Literal)和符号引用(Symbolic Reference)收集起来,存放在 Class 文件的常量池表中 。
  • 内容
    • 字面量:文本字符串、final 常量值等。
    • 符号引用:类和接口的全限定名、字段名称和描述符、方法名称和描述符等 。
  • 特点:静态、不可变,是类文件的“资源清单”。

2. 运行时常量池(Runtime Constant Pool)

  • 存在形式:JVM 内存中的一块区域。
  • 生成时机:类加载阶段。JVM 将 Class 常量池的内容加载到内存中,形成运行时常量池 。
  • 位置:属于**方法区(Method Area)的一部分。在 JDK 8+ 中,方法区由元空间(Metaspace)**实现 。
  • 内容:包含从 Class 文件加载的字面量和符号引用。在类加载的“解析”阶段,符号引用会被替换为指向内存具体地址的直接引用
  • 特点动态性。除了预置入 Class 文件的常量外,运行期间也可以将新的常量放入池中(如 String.intern()) 。每个类都有独立的运行时常量池。

3. 字符串常量池(String Pool / String Table)

  • 存在形式:JVM 内部维护的一个哈希表(StringTable)。
  • 位置演变
    • JDK 1.6 及之前:位于方法区(永久代)。
    • JDK 1.7 及之后:移至堆内存(Heap)
  • 内容:存储字符串对象的引用(JDK 7+)或实例本身(JDK 6-),确保相同内容的字符串全局唯一 。
  • 特点全局共享。所有类共用同一个字符串常量池,旨在减少字符串对象的重复创建 。

二、 核心差异对比

特性Class 常量池运行时常量池字符串常量池
存在阶段编译期(磁盘文件)运行期(内存)运行期(内存)
存储位置.class 文件方法区(元空间)堆(JDK7+)/ 方法区(JDK6-)
作用域单个 Class 文件单个加载的类JVM 全局唯一
主要 content字面量、符号引用字面量、直接引用字符串对象引用/实例
动态性无(静态)有(可动态添加)有(可动态添加)

三、 String.intern() 深度解析

String.intern() 是连接运行时字符串与常量池的关键桥梁。

1. 作用机制

当调用 str.intern() 时:

  1. JVM 检查字符串常量池中是否存在与 str 内容相等的字符串。
  2. 若存在:返回池中已有字符串的引用。
  3. 若不存在
    • JDK 6-:在永久代复制一份字符串实例,返回新实例引用。
    • JDK 7+:在堆中记录当前字符串对象的引用,返回该引用(不再复制对象) 。

2. 代码实战与内存分析

以下代码基于 JDK 8+ 环境执行:

public class StringInternDemo {
    public static void main(String[] args) {
        // 场景1:字面量赋值
        String s1 = "abc"; 
        String s2 = "abc";
        System.out.println(s1 == s2); // true,都指向字符串常量池中的同一个对象

        // 场景2:new String() 与 intern
        String s3 = new String("abc"); 
        // "abc" 字面量在加载时已进入字符串常量池。
        // s3 是堆中新建的对象,地址不同。
        System.out.println(s1 == s3); // false

        String s4 = s3.intern(); 
        // 检查字符串常量池,发现 "abc" 已存在(由 s1 触发加载)
        // 返回池中已有的引用,即 s1 的引用
        System.out.println(s1 == s4); // true
        System.out.println(s3 == s4); // false

        // 场景3:动态拼接与 intern (重点)
        String s5 = new String("de") + new String("f"); // 堆中对象 "def"
        String s6 = s5.intern(); 
        // 字符串常量池中原本没有 "def"
        // JDK7+:将 s5 的引用加入字符串常量池,返回 s5 的引用
        System.out.println(s5 == s6); // true

        String s7 = "def"; 
        // 字面量 "def" 去字符串常量池查找,发现已有引用(由 s6 放入)
        // 返回该引用,即 s5 的引用
        System.out.println(s5 == s7); // true
    }
}

3. 关键结论

  • JDK 6 vs JDK 7+ 的区别:在 JDK 6 中,s5 == s6 会为 false,因为 intern() 会复制对象到永久代;而在 JDK 7+ 中,intern() 仅记录引用,因此 s5s6 指向堆中同一个对象 。
  • 运行时常量池的动态性String.intern() 体现了运行时常量池可以在运行时动态添加常量的特性,这是 Class 常量池无法做到的 。

四、 三者之间的联系与工作流

  1. 加载阶段:JVM 读取 .class 文件,将 Class 常量池 中的符号引用和字面量加载到 运行时常量池
  2. 解析阶段:运行时常量池中的符号引用被解析为直接引用。对于字符串字面量,JVM 会查询 字符串常量池,确保运行时常量池中的引用指向字符串常量池中的唯一对象 。
  3. 运行阶段
    • 当代码执行 String s = "abc" 时,JVM 直接利用解析好的直接引用。
    • 当执行 new String("abc") 时,会在堆中创建新对象。
    • 当执行 s.intern() 时,动态操作 字符串常量池,实现运行时字符串的全局共享 。

五、 总结

  • Class 常量池是静态的蓝图,存在于磁盘。
  • 运行时常量池是类加载后的内存表示,位于元空间,具有动态性。
  • 字符串常量池是全局的字符串缓存,位于堆(JDK7+),用于优化内存。
  • String.intern() 是手动干预字符串常量池的手段,合理利用可节省内存,但需注意在高并发下 StringTable 锁竞争带来的性能影响 。

理解这三者的关系,不仅有助于应对面试,更能帮助开发者在高性能场景下做出正确的内存管理决策。

0

评论区