JVM 中的常量池到底在哪儿
在 JDK 8 中:
- 运行时常量池(类的常量信息)在 元空间(Metaspace)(属于方法区);
- 字符串常量池 在 堆(Heap)。
所以不能简单说 “常量池在堆” 或 “在方法区” ,要区分类型。
结论
| 常量池类型 | 存储位置 | 说明 |
|---|---|---|
| 运行时常量池 | 元空间(Metaspace) | 属于 方法区 的一部分 |
| 字符串常量池 | 堆(Heap) | 存放字符串字面量,位于 堆 中 |
详细解释
1. 运行时常量池
- 是类加载后,类文件常量池 被加载到 JVM 内存中的运行时版本。
- 存储:类级别的常量信息,如:
- 字面量(
int a = 100;中的100) - 符号引用(类名、方法名、字段名)
- 方法句柄、动态调用信息等
- 字面量(
- 位置:元空间(Metaspace)
在 JDK 8 中,方法区(Method Area)的实现是“元空间”(Metaspace),它使用本地内存(Native Memory),不再属于 JVM 堆。
所以:运行时常量池 → 方法区 → 元空间
2. 字符串常量池
- 存储:字符串字面量(如
"hello") - 示例:
String s = "hello"; // "hello" 存在字符串常量池中 - 位置:堆(Heap)
这是 JDK 7 开始的改变!
- JDK 6 及之前:字符串常量池在 永久代(PermGen)
- JDK 7:字符串常量池被移到 堆
- JDK 8:继续在 堆,而永久代被彻底移除,由 元空间 取代
为什么字符串常量池要放在堆里?
- 永久代垃圾回收效率低,而字符串大量使用,容易造成内存问题。
- 堆的垃圾回收更高效,字符串常量池放在堆里更利于回收不再使用的字符串。
- 元空间使用本地内存,主要用于存储类的元数据(如类结构、方法字节码等),不适合放大量字符串。
示例验证
public class ConstantPoolTest {
public static void main(String[] args) {
String s1 = "hello"; // 字符串常量池 → 堆
Integer i = 100; // Integer 缓存池 → 堆(但缓存机制不同)
Class<?> clazz = ConstantPoolTest.class;
// 运行时常量池中的类名、方法名等 → 元空间
}
}"hello"→ 堆(字符串常量池)ConstantPoolTest.class的类名、方法名等符号 → 元空间(运行时常量池)
再次总结
在 JDK 8 中:
- 运行时常量池(类的常量信息)在 元空间(Metaspace)(属于方法区);
- 字符串常量池 在 堆(Heap)。
所以不能简单说“常量池在堆”或“在方法区”,要区分类型。
案例说明
public static void main(String[] args) {
String a = new StringBuilder("yunze").toString();
String b = "yunze";
String c = a.intern();
System.out.println(a == b); // false
System.out.println(a == c); // false
System.out.println(b == c); // true
}String中的intern方法是一个 native 的方法,当调用 intern方法时,如果池已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回池中的字符串。否则,将intern返回的引用指向当前字符串 s1。
| 变量 | 来源 | 指向的对象位置 | 说明 |
|---|---|---|---|
a | new StringBuilder("yunze").toString() | 堆 | 是 StringBuilder.toString() 创建的全新对象,与常量池无关。 |
b | "yunze" | 字符串常量池 | 是字符串字面量,直接来自常量池。 |
c | a.intern() | 字符串常量池 | intern() 方法拿到的是常量池里对象。 |
