在 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() 时:
- JVM 检查字符串常量池中是否存在与
str内容相等的字符串。 - 若存在:返回池中已有字符串的引用。
- 若不存在:
- 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()仅记录引用,因此s5和s6指向堆中同一个对象 。 - 运行时常量池的动态性:
String.intern()体现了运行时常量池可以在运行时动态添加常量的特性,这是 Class 常量池无法做到的 。
四、 三者之间的联系与工作流
- 加载阶段:JVM 读取
.class文件,将 Class 常量池 中的符号引用和字面量加载到 运行时常量池 。 - 解析阶段:运行时常量池中的符号引用被解析为直接引用。对于字符串字面量,JVM 会查询 字符串常量池,确保运行时常量池中的引用指向字符串常量池中的唯一对象 。
- 运行阶段:
- 当代码执行
String s = "abc"时,JVM 直接利用解析好的直接引用。 - 当执行
new String("abc")时,会在堆中创建新对象。 - 当执行
s.intern()时,动态操作 字符串常量池,实现运行时字符串的全局共享 。
- 当代码执行
五、 总结
- Class 常量池是静态的蓝图,存在于磁盘。
- 运行时常量池是类加载后的内存表示,位于元空间,具有动态性。
- 字符串常量池是全局的字符串缓存,位于堆(JDK7+),用于优化内存。
String.intern()是手动干预字符串常量池的手段,合理利用可节省内存,但需注意在高并发下StringTable锁竞争带来的性能影响 。
理解这三者的关系,不仅有助于应对面试,更能帮助开发者在高性能场景下做出正确的内存管理决策。
评论区