一、常量池分类
| 类型 | 存储位置 | 触发时机 | 内容 | 生命周期 | 线程共享 |
|---|---|---|---|---|---|
| 字节码常量池 | .class 文件 | 编译期 | 类名、方法名、字符串字面量等 | 持久化磁盘 | 否 |
| 运行时常量池 | 方法区(Metaspace) | 类加载时 | 符号引用 → 直接引用 | JVM 运行期间 | 是 |
| 字符串常量池 | 堆(JDK 7+) | 首次使用字符串 | String 对象引用缓存 | JVM 运行期间 | 是 |
✅ 字节码常量池:静态只读
✅ 运行时常量池:动态可扩展
✅ 字符串常量池:专用于String优化,是运行时常量池的特化子集
二、数据结构与解析
字节码常量池
-
.class文件开头,索引从 1 开始 -
结构:
CONSTANT_Utf8_info、CONSTANT_String_info等 -
示例:
#3 = String #20 // "Hello" #4 = Class #21 // java/lang/System
运行时常量池
- 每个类一个,位于方法区
- 解析阶段将符号引用转为直接引用(如方法地址、字段偏移)
- 支持动态扩展(如
invokedynamic)
字符串常量池
- 底层:哈希表(数组 + 链表)
- JDK 6:永久代 → GC 慢,OOM 风险高
- JDK 7+:堆中 → 受常规 GC 管理
三、intern() 行为(JDK 7+)
String s1 = new String("hello"); // 堆对象
String s2 = s1.intern(); // 返回串池中 "hello" 引用
String s3 = "hello"; // 自动入池
s1 == s2 → false
s2 == s3 → true
"hello"字面量在类加载时已入池new String("hello")创建额外堆对象intern()不新建对象,仅注册引用(JDK 7+)
四、执行流程(HelloWorld)
System.out.println("Hello, World!");
- 编译 →
.class中含#3 = String #18 - 类加载 → 创建运行时常量池
- 执行
ldc #3→ 首次使用"Hello, World!",创建对象并入串池 - 调用
println(方法引用已解析)
五、关键对比
| 维度 | 运行时常量池 | 字符串常量池 |
|---|---|---|
| 范围 | 所有类/方法/字段 | 仅 String |
| 内容 | 直接引用(指针/偏移) | String 对象引用 |
| 创建时机 | 类加载时 | 字符串首次使用(懒加载) |
| 位置 | Metaspace | 堆(JDK 7+) |
| GC | 类卸载时释放 | 普通堆 GC |
协作链:
字面量 → 字节码常量池 → 运行时常量池 → 字符串常量池(按需)
六、典型面试题
1. 引用比较
String a = "hello";
String b = new String("hello");
String c = b.intern();
a == b → false
a == c → true
2. 编译期折叠
String s1 = "ja" + "va"; // → "java"(编译期确定)
s1 == "java" → true
3. 运行时拼接
String x = "ja", y = "va";
String z = x + y; // StringBuilder 新建对象
z == "java" → false
七、OOM 场景与防范
运行时常量池 OOM(Metaspace)
- 原因:动态生成大量类(CGLib、JSP)
- 表现:
OutOfMemoryError: Metaspace - 对策:
-XX:MaxMetaspaceSize=256m- 避免类加载器泄漏
字符串常量池 OOM
- 原因:大量唯一字符串调用
intern() - 表现:堆内存耗尽
- 对策:
- 不对 UUID、时间戳等高熵字符串
intern() - 使用软引用缓存替代
- 不对 UUID、时间戳等高熵字符串
八、调优与监控
建议
- ✅ 优先用字面量(自动入池)
- ✅ 高频重复字符串主动
intern() - ❌ 避免动态字符串无脑
intern()
监控命令
jcmd <pid> VM.metaspace # Metaspace 使用
jstat -class <pid> # 类加载统计
jmap -histo:live <pid> | grep String
关键参数
-XX:MaxMetaspaceSize=512m
-XX:StringTableSize=65536 # 默认 60013,建议质数
九、执行全景图
源码
→ .class(字节码常量池)
→ 类加载(运行时常量池)
→ 解析(符号→直接引用)
→ 首次 ldc(字符串入池)
→ 执行引擎调用
核心:常量池是 JVM “符号 → 实体” 的转换枢纽。
掌握三层结构 +intern()差异 + 内存模型,即可精准调优。