java常量池
一 常量池示例
/**
* 字符串:"奶茶" 存在堆内存的 字符串常量池。
* 其他常量(如数字、类名):存在方法区(元空间)的 运行时常量池。
*
*/
public static void main(String[] args) {
String s1 = "奶茶"; // 第一次写“奶茶”,存到公共菜单(常量池)
String s2 = "奶茶"; // 直接拿菜单上的“奶茶”,不用新做一杯
String s3 = new String("奶茶"); // 强行做一杯新的,但原料还是菜单上的“奶茶” 。重新创建了一个对象,堆中的地址不同。
先在堆常量池创建一份(如果没有),然后再在堆中创建一个对象。
// s1="12";
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s1 .equals(s2) );
System.out.println(s1 .equals(s3) );
}
}
二 说明
1
创建方式的本质区别
s1 = “奶茶”
这是通过字符串字面量(String Literal)创建的字符串。
JVM 会先在字符串常量池(位于堆内存中)中查找是否有 “奶茶”:
如果存在,直接复用该对象(地址相同)。
如果不存在,在常量池中新建一个对象。
特点:多个相同的字面量会指向同一个对象。
2
s3 = new String(“奶茶”)
这是通过 new 关键字在堆内存中强制创建新对象。
JVM 会:
先在字符串常量池中检查 “奶茶” 是否存在(如果不存在,则先在常量池中创建)。
然后在堆内存中创建一个全新的 String 对象,内容与 “奶茶” 相同,但地址不同。
特点:每次 new 都会生成新对象,即使内容相同。
二 常量池存储位置
1
public class Example {
// 运行时常量池(元空间):存储符号引用和字面量 "Hello"
private static final String MESSAGE = "Hello";
public static void main(String[] args) {
// 字符串常量池(堆内存):存储 "Hello"
String s1 = "Hello";
// 堆内存:创建新对象,但引用字符串常量池的 "Hello"
String s2 = new String("Hello");
// 字符串常量池(堆内存):新增 "World"
String s3 = "World";
}
}
二 运行时常量
1 运行时常量池 ≠ static final 变量
1. static final 变量的存储
基本类型(如 static final int MAX = 100):
值直接存储在运行时常量池中。
引用类型(如 static final String S = “OK”):
引用(指针)存储在运行时常量池中,实际对象(如 “OK”)在堆内存的字符串常量池中。
2
运行时常量池的内容远不止 static final
除了 static final 变量,运行时常量池还包含:
类的符号引用:如 java/lang/String。
方法的符号引用:如 java/io/PrintStream.println。
动态生成的常量:如通过 String.intern() 添加的字符串。
字段的符号引用:如 MyClass.count。
字面量:如代码中直接写的 “Hello” 或 123。
三 关键总结
1 Class 文件常量池:
静态数据,存储在 .class 文件中,包含符号引用和字面量。
例如:java/lang/String 的符号引用、字符串 “Hello” 的字面量。
2 运行时常量池:
动态数据,存储在方法区(元空间),包含解析后的直接引用和运行时生成的常量。
不仅仅是 static final 变量:还包含类、方法、字段的元数据,以及动态内容(如 intern() 的字符串)。
3 static final 变量的特殊性:
基本类型的 static final 变量直接存储在运行时常量池。
引用类型的 static final 变量存储引用(在运行时常量池),对象在堆中。
4 Class 文件常量池 vs 运行时常量池
核心区别
对比项 | Class 文件常量池 | 运行时常量池 |
---|---|---|
存储位置 | 编译后的 .class 文件中(静态文件) | JVM 内存的方法区(元空间)中(动态运行时数据) |
存储内容 | 符号引用(类名、方法名、字段名等)、字面量(数值、字符串) | Class 文件常量池的副本 + 运行期动态添加的常量(如 String.intern() 的字符串) |
生命周期 | 永久(随 .class 文件存在) | 类加载时创建,类卸载时销毁 |
是否可修改 | 不可修改(静态编译数据) | 可动态扩展(例如运行时添加新的常量) |
符号引用解析 | 未解析(如 java/lang/Object ) | 已解析为直接引用(如内存地址) |
四 常见误区
1
误区:认为运行时常量池只存 static final 变量。
纠正:运行时常量池的核心是类元数据,static final 变量只是其中一部分。
2
误区:认为 static final String S = new String(“OK”) 的 “OK” 在运行时常量池。
纠正:new String(“OK”) 的 “OK” 在堆中,但字面量 “OK” 的引用在运行时常量池。
五 String s2 = new String(“Hello”);为什么创建两个对象?
1 为什么会有两个对象?
当执行 String s2 = new String(“Hello”); 时,JVM 会执行以下步骤:
检查字符串常量池:
首先检查字符串常量池中是否存在字面量 “Hello”。
如果存在,直接复用该对象。
如果不存在,在字符串常量池中创建一个新的 “Hello” 对象。
在堆中创建新对象:
无论常量池中是否存在 “Hello”,new String(“Hello”) 都会在堆内存中创建一个全新的 String 对象,内容与常量池中的 “Hello” 相同。
结果:
常量池对象:字面量 “Hello” 的对象(共享)。
堆对象:new 创建的全新 String 对象(独立)。
2 看似重复,但逻辑合理:
字符串常量池的设计是为了 共享不可变对象,减少内存占用。
new 关键字的设计是为了 显式创建独立对象,满足某些特殊场景需求(如需要不同实例)。
六 字符串常量池在堆内存中吗?
是的!Java 7 及之后版本,字符串常量池的位置从永久代(PermGen)迁移到了堆内存(Heap)。
Java 6 及之前:字符串常量池在永久代。
Java 7+:字符串常量池在堆内存中。
迁移原因:
永久代大小固定:容易导致 OutOfMemoryError: PermGen space。
堆内存动态扩展:字符串常量池可以随堆内存动态调整,避免内存溢出。
垃圾回收:堆内存中的字符串常量池对象可以被垃圾回收(无引用时)。
2 内存结构示意图
+---------------------+ +---------------------+
| 字符串常量池(堆内存) | | 堆内存 |
|---------------------| |---------------------|
| String对象 "Hello" | <---- s1(引用地址相同) |
+---------------------+ | |
| new String("Hello") | <---- s2(全新对象)
+---------------------+
• s1 = "Hello"
:直接指向字符串常量池中的对象。
• s2 = new String("Hello")
:指向堆中的新对象,但字面量 "Hello"
会先在常量池中创建(如果不存在)。
3 总结
操作 | 结果 |
---|---|
String s1 = "Hello"; | 直接使用字符串常量池中的对象(无重复创建)。 |
String s2 = new String("Hello"); | 先在常量池中创建 "Hello" (如不存在),再在堆中创建新对象(可能重复,但逻辑合理)。 |
• 字符串常量池在堆内存中:Java 7+ 的优化设计,避免永久代内存溢出。
• 重复创建的合理性:new
关键字的设计目标与常量池不同,需根据场景选择。