问题
运行时常量池和字符串常量池的关系是什么?
答案
核心概念
- 运行时常量池(Runtime Constant Pool):每个类加载后在方法区创建的常量池,存储该类的常量信息
- 字符串常量池(String Pool/String Table):JVM全局唯一的字符串缓存池,用于存储字符串字面量的引用
关系:字符串常量池是独立于运行时常量池的全局结构,运行时常量池中的字符串字面量会在字符串常量池中维护引用。
关键区别
| 对比项 | 运行时常量池 | 字符串常量池 |
|---|---|---|
| 作用域 | 每个类独立 | 全局唯一(所有类共享) |
| 存储位置(JDK 8+) | 元空间(Metaspace) | 堆(Heap) |
| 存储内容 | 字面量、符号引用、直接引用 | 字符串对象的引用 |
| 数据结构 | 数组/表结构 | HashTable |
| 生命周期 | 跟随类的生命周期 | 跟随JVM生命周期 |
| 可见性 | 类级别 | JVM全局 |
存储位置的演变
JDK 7之前
永久代(PermGen)
├── 运行时常量池(每个类一个)
└── 字符串常量池
JDK 7
元空间(Metaspace)
└── 运行时常量池
堆(Heap)
└── 字符串常量池 ← 移至堆
JDK 8及之后
元空间(Metaspace)
└── 运行时常量池
堆(Heap)
└── 字符串常量池
关键变化:JDK 7将字符串常量池从永久代移到堆,避免永久代OOM问题。
字符串常量池的特殊性
1. 全局唯一性
public class StringPoolDemo {
public static void main(String[] args) {
// 两个不同类中的相同字符串字面量
String s1 = "hello"; // ClassA的运行时常量池
String s2 = "hello"; // ClassB的运行时常量池
// 实际上都指向字符串常量池中的同一个对象
System.out.println(s1 == s2); // true
}
}
原理:
ClassA运行时常量池 → "hello"引用 ↘
字符串常量池中的"hello"对象
ClassB运行时常量池 → "hello"引用 ↗
2. intern()方法的作用
public class InternDemo {
public static void main(String[] args) {
// 堆中创建新对象
String s1 = new String("hello");
// intern()尝试将字符串加入字符串常量池
String s2 = s1.intern();
// 字符串字面量直接来自常量池
String s3 = "hello";
System.out.println(s1 == s2); // false(s1在堆,s2在常量池)
System.out.println(s2 == s3); // true(都来自字符串常量池)
}
}
运行时常量池与字符串常量池的交互
示例1:字符串字面量的加载
public class StringLoadingDemo {
public static void main(String[] args) {
String s1 = "hello"; // 触发字符串加载
}
}
加载流程:
1. 编译期:
"hello"存入Class文件的Class常量池
2. 类加载期:
├─ Class常量池 → ClassA的运行时常量池
└─ 检查字符串常量池是否有"hello"
├─ 没有:在堆中创建"hello"对象,将引用存入字符串常量池
└─ 有:直接使用已有引用
3. 执行期:
s1 = 字符串常量池中"hello"的引用
示例2:跨类字符串共享
// ClassA.java
public class ClassA {
public static String getGreeting() {
return "hello"; // 来自ClassA的运行时常量池
}
}
// ClassB.java
public class ClassB {
public static String getGreeting() {
return "hello"; // 来自ClassB的运行时常量池
}
}
// Main.java
public class Main {
public static void main(String[] args) {
String s1 = ClassA.getGreeting();
String s2 = ClassB.getGreeting();
String s3 = "hello";
// 都指向字符串常量池的同一个对象
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // true
}
}
JDK 7的关键变化详解
变化1:字符串常量池位置迁移
// JDK 6
public class JDK6StringPoolDemo {
public static void main(String[] args) {
String s = "hello";
// s引用的对象在永久代的字符串常量池
}
}
// JDK 7+
public class JDK7StringPoolDemo {
public static void main(String[] args) {
String s = "hello";
// s引用的对象在堆中的字符串常量池
}
}
好处:
- 永久代空间有限,容易OOM
- 堆空间更大,GC效率更高
- 避免
-XX:PermSize调优困扰
变化2:intern()行为变化
public class InternBehaviorDemo {
public static void main(String[] args) {
String s1 = new String("a") + new String("b"); // 堆中"ab"
String s2 = s1.intern();
String s3 = "ab";
System.out.println(s1 == s2); // JDK 6: false, JDK 7+: true
System.out.println(s2 == s3); // true
}
}
JDK 6行为:
intern() → 在永久代字符串常量池创建"ab"副本 → 返回副本引用
s1(堆)!= s2(永久代)
JDK 7+行为:
intern() → 字符串常量池存储s1的引用(不复制对象)→ 返回s1引用
s1(堆)== s2(字符串常量池存的是s1的引用)
实战示例
示例1:常量池膨胀问题
public class StringPoolInflationDemo {
public static void main(String[] args) {
// 危险:大量intern()导致字符串常量池膨胀
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000000; i++) {
list.add(String.valueOf(i).intern()); // 1000万个字符串进入常量池
}
// 可能导致性能下降或OOM
}
}
优化:
// 避免不必要的intern()
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000000; i++) {
list.add(String.valueOf(i)); // 不使用intern()
}
示例2:查看字符串常量池大小
# JDK 7+
java -XX:+PrintStringTableStatistics -version
# 输出
StringTable statistics:
Number of buckets : 60013 = 480104 bytes
Number of entries : 1765 = 42360 bytes
Number of literals : 1765 = 156440 bytes
调优参数:
# 设置字符串常量池大小(bucket数量)
-XX:StringTableSize=1000003
# 默认值
# JDK 7: 60013
# JDK 8: 60013
# JDK 9+: 动态调整
内存泄漏风险
public class StringPoolLeakDemo {
// 危险:大量长字符串intern()
public void processLogs(List<String> logs) {
for (String log : logs) {
// 每条日志都很长(几KB),且不重复
cache.put(log.intern(), parseLog(log));
// 字符串常量池持有这些大对象引用,无法GC
}
}
}
正确做法:
public void processLogs(List<String> logs) {
for (String log : logs) {
// 不使用intern(),让字符串可以被GC
cache.put(log, parseLog(log));
}
}
判断字符串是否在常量池
public class StringPoolCheckDemo {
public static void main(String[] args) {
String s1 = "hello";
String s2 = new String("world");
String s3 = s2.intern();
// 方式1:比较intern()结果
System.out.println(s1 == s1.intern()); // true(已在常量池)
System.out.println(s2 == s2.intern()); // false(不在常量池)
System.out.println(s3 == s3.intern()); // true(已在常量池)
// 方式2:与字面量比较
System.out.println(s1 == "hello"); // true
System.out.println(s2 == "world"); // false
System.out.println(s3 == "world"); // true
}
}
数据结构对比
运行时常量池
// 简化的结构(概念示意)
class RuntimeConstantPool {
Object[] constants; // 数组存储
int size;
Object getConstant(int index) {
return constants[index];
}
}
字符串常量池
// 简化的结构(概念示意)
class StringTable {
Entry[] buckets; // HashTable结构
int size;
static class Entry {
int hash;
String value;
Entry next; // 链表解决冲突
}
String intern(String str) {
int hash = str.hashCode();
int index = hash % buckets.length;
// 查找或插入
}
}
面试要点总结
- 核心区别:运行时常量池是类级别的,字符串常量池是JVM全局唯一的
- 存储位置:JDK 7+字符串常量池在堆,运行时常量池在元空间
- 关系:运行时常量池中的字符串字面量引用指向字符串常量池中的对象
- intern()行为:JDK 7+存储引用而非复制对象(重要变化)
- 性能考量:避免大量intern()导致字符串常量池膨胀
- 调优参数:
-XX:StringTableSize调整字符串常量池大小 - 内存泄漏风险:大量不重复字符串intern()会导致内存泄漏