运行时常量池:类的“动态小抄本”
1. 是什么?
-
角色:方法区中的一块“动态笔记区”,存放类的字面量(如字符串、数字)和符号引用(如类名、方法名)。
-
来源:编译时写入(从Class文件的常量池加载) + 运行时动态添加(如通过
String.intern()
)。 -
比喻:像一本随时可以补充笔记的课本——既有原本印刷的内容(编译期常量),也能手写添加新内容(运行期常量)。
2. Class文件常量池 vs 运行时常量池
-
Class文件常量池:
-
位置:
.class
文件中。 -
内容:固定的字面量(如
"Hello"
)和符号引用(如java/lang/Object
)。 -
作用:告诉JVM“这个类需要用到哪些常量和其他类”。
-
-
运行时常量池:
-
位置:方法区(JDK8前是永久代,JDK8后是元空间)。
-
内容:Class文件常量池的副本 + 运行期动态加入的常量(如
String.intern()
生成的字符串)。 -
动态性:可以随时往池子里加新内容!
-
示例:
java
复制
String s1 = "Hello"; // "Hello"来自Class文件常量池 String s2 = new String("World").intern(); // "World"通过intern()动态加入运行时常量池
3. 符号引用 vs 直接引用
-
符号引用:用“名字”表示目标(如
java/lang/String
)。-
类比:用“张三家的地址:XX小区1号楼”描述一个位置。
-
-
直接引用:目标在内存中的实际地址(如
0x7f3e8c
)。-
类比:GPS坐标(
纬度30.123, 经度120.456
)。
-
JVM的转换过程:
类加载时,JVM把符号引用解析为直接引用(把“XX小区1号楼”翻译成具体坐标),并存到运行时常量池。
4. 动态性示例:String.intern()
-
作用:将字符串添加到运行时常量池,并返回池中的引用。
-
场景:避免重复创建相同字符串,节省内存。
代码示例:
java
复制
String s1 = new String("Hello"); // 在堆中创建新对象 String s2 = s1.intern(); // 将"Hello"加入常量池(如果不存在) System.out.println(s1 == s2); // false(s1在堆,s2指向常量池) String s3 = "Hello"; System.out.println(s2 == s3); // true(都指向常量池)
5. 为什么会导致OutOfMemoryError(OOM)?
-
触发条件:运行时常量池所在的方法区(或元空间)内存耗尽。
-
典型场景:
-
JDK8前:永久代容量有限(默认较小),频繁调用
intern()
加载大量字符串。 -
JDK8后:元空间默认无上限,但若系统内存不足,仍会OOM。
-
示例:
java
复制
List<String> list = new ArrayList<>(); while (true) { String s = new String("疯狂添加" + i).intern(); // 不断往常量池塞字符串 list.add(s); }
最终元空间(或永久代)被撑爆 → 抛出OOM!
总结对比表
特性 | Class文件常量池 | 运行时常量池 |
---|---|---|
存储位置 | .class 文件中 | 方法区(元空间/永久代) |
内容 | 编译期确定的字面量和符号引用 | Class常量池内容 + 运行期动态添加的常量 |
动态性 | 固定不变 | 可动态扩展(如intern() ) |
内存限制 | 无(文件大小限制) | 受方法区/元空间内存限制,可能OOM |
一句话理解
运行时常量池是类的“动态小抄本”——原本抄写课本内容,运行时还能自己加笔记。但笔记本(方法区)写满了就会爆仓(OOM)!