目录
- java 常量池详解
- 一 静态常量池(Static Constant Pool)
- 1.1 概述
- 1.2 存储内容
- 1.3 特点
- 1.4 示例
- 二 运行时常量池(Runtime Constant Pool)
- 2.1 概述
- 2.2 存储内容
- 2.3 特点
- 2.4 示例
- 三 基础类型常量池(Primitive Type Constant Pool)
- 3.1 概述
- 3.2 存储内容
- 3.3 特点
- 3.4 示例
- 3.5 JVM 参数调整缓存范围
- 四 字符串常量池(String Constant Pool)
- 4.1 概述
- 4.2 特点
- 4.3 存储内容
- 4.4 工作机制
- 1. 字符串字面量
- 2. `String.intern()` 方法
- 4.5 优点
- 4.6 注意事项
- 五 常量池的关系与区别
- 六 总结
java 常量池详解
在 Java JVM(以 JDK 11 为例)中,常量池是优化内存使用和提高性能的重要机制。常量池主要包括静态常量池、运行时常量池、基础类型常量池和字符串常量池。以下将对这四种常量池进行详细整理和介绍。
一 静态常量池(Static Constant Pool)
1.1 概述
静态常量池是指每个 .class
文件中包含的常量池部分,由 Java 编译器在编译时生成。它主要用于存储在编译期间已知的各种字面量和符号引用。
1.2 存储内容
- 字面量(Literals):
- 数值字面量:如
int
、long
、float
、double
、char
、boolean
的常量值。 - 字符串字面量:如
"Hello World"
。
- 数值字面量:如
- 符号引用(Symbolic References):
- 类或接口的全限定名:如
java/lang/String
。 - 字段的名称和描述符:如
age
和I
(表示int
类型)。 - 方法的名称和描述符:如
main
和([Ljava/lang/String;)V
。
- 类或接口的全限定名:如
1.3 特点
- 编译期生成:静态常量池在编译时就已经确定,并存储在
.class
文件中。 - 不可变性:一旦编译完成,静态常量池中的内容不可更改。
- 类级别:每个类文件对应一个静态常量池,多个类文件拥有各自独立的静态常量池。
1.4 示例
假设有如下 Java 类:
public class Example {private static final int CONSTANT_INT = 100;private static final String CONSTANT_STRING = "Hello";public static void main(String[] args) {System.out.println(CONSTANT_INT);System.out.println(CONSTANT_STRING);}
}
在编译后的 Example.class
文件中,静态常量池会包含:
- 整数
100
- 字符串
"Hello"
- 类
java/lang/System
- 字段
out
和println
方法的符号引用
二 运行时常量池(Runtime Constant Pool)
2.1 概述
运行时常量池是 JVM 在类加载后,静态常量池的一个运行时表示。它是方法区的一部分(在 JDK 8 及之后版本中,属于元空间 Metaspace),用于在程序运行时管理和使用常量。
2.2 存储内容
- 来自静态常量池的内容:
- 字面量(如数值、字符串)。
- 符号引用解析后的直接引用(如指向内存地址的指针)。
- 动态生成的内容:
- 通过
String.intern()
方法动态添加的字符串。 - 动态语言的元数据(如在 JVM 上运行的动态语言)。
- 通过
2.3 特点
- 动态性:可以在运行时动态添加新的常量,如通过
intern()
方法。 - 内存位置:在 JDK 8 及之后版本中,运行时常量池位于堆内存的元空间中。
- 生命周期:与类的生命周期相关,类被卸载时,常量池也会被回收。
2.4 示例
public class RuntimePoolTest {public static void main(String[] args) {String str1 = "Hello"; // 静态常量String str2 = new String("Hello").intern(); // 动态添加到运行时常量池}
}
在上述代码中:
"Hello"
是编译时的字符串字面量,已存在于静态常量池。str2
通过intern()
方法将"Hello"
的引用添加到运行时常量池。
三 基础类型常量池(Primitive Type Constant Pool)
3.1 概述
基础类型常量池主要针对 Java 中的基本数据类型(如 int
、long
等)以及其对应的包装类(如 Integer
、Long
等),用于优化内存使用和提高性能。
3.2 存储内容
- 基本数据类型的常量值:
- 如整数
1
、浮点数3.14
、字符'A'
、布尔值true
等。
- 如整数
- 包装类的缓存对象:
- 包装类如
Integer
、Long
、Short
、Byte
、Character
、Boolean
提供了缓存机制,存储常用的常量值。
- 包装类如
3.3 特点
-
缓存机制:包装类会缓存一定范围内的常量对象,以减少内存开销和提升性能。
-
范围限制
:
Integer
和Long
:默认缓存范围是-128
到127
。Short
和Byte
:所有值都在缓存范围内。Character
:缓存范围是\u0000
到\u007F
(即0
到127
)。Boolean
:只有true
和false
两个值被缓存。
-
可调整性:某些包装类的缓存范围可以通过 JVM 参数进行调整(如
Integer
的缓存上限)。
3.4 示例
public class PrimitiveConstantPoolTest {public static void main(String[] args) {Integer a = 127;Integer b = 127;Integer c = 128;Integer d = 128;System.out.println(a == b); // true,引用同一个缓存对象System.out.println(c == d); // false,超出缓存范围,创建新对象}
}
在上述代码中:
a
和b
都被自动装箱为Integer
类型,且值在缓存范围内,因此引用同一个对象。c
和d
的值超出默认缓存范围,因此分别创建了不同的对象。
3.5 JVM 参数调整缓存范围
对于 Integer
,可以通过以下 JVM 参数调整缓存上限:
-Djava.lang.Integer.IntegerCache.high=1000
这会将 Integer
的缓存范围扩展到 -128
到 1000
。
四 字符串常量池(String Constant Pool)
4.1 概述
字符串常量池是 JVM 中专门用于存储字符串字面量和通过 intern()
方法添加的字符串的区域。其主要目的是为了优化字符串的存储,避免重复创建相同内容的字符串对象,从而节省内存并提高性能。
4.2 特点
- 位置:
- JDK 7 及之后版本:字符串常量池位于堆内存中,作为普通对象的一部分。
- JDK 6 及之前版本:字符串常量池位于方法区(永久代 PermGen)。
- 唯一性:
- 常量池中的每个字符串都是唯一的。如果两个字符串内容相同,常量池中只存储一个实例。
- 动态性:
- 可以在运行时通过
String.intern()
方法动态添加新的字符串到常量池中。
- 可以在运行时通过
4.3 存储内容
- 字符串字面量:如
"Hello"
、"World"
等。 - 通过
intern()
方法添加的字符串。
4.4 工作机制
1. 字符串字面量
当创建字符串字面量时,JVM 会:
- 检查常量池:首先检查字符串常量池中是否已存在相同内容的字符串。
- 引用或添加:
- 如果存在,直接引用常量池中的字符串。
- 如果不存在,将该字符串添加到常量池中。
示例代码:
public class StringPoolTest {public static void main(String[] args) {String str1 = "Hello";String str2 = "Hello";System.out.println(str1 == str2); // 输出 true,引用同一个对象}
}
2. String.intern()
方法
- 功能:将字符串对象的引用添加到常量池中,并返回常量池中的引用。
- 行为:
- 如果常量池中已存在相同内容的字符串,则返回该引用。
- 如果不存在,则将当前字符串添加到常量池中,并返回其引用。
示例代码:
public class StringInternTest {public static void main(String[] args) {String str1 = new String("Hello");String str2 = str1.intern();String str3 = "Hello";System.out.println(str1 == str2); // 输出 false,str1 在堆中,str2 引用常量池System.out.println(str2 == str3); // 输出 true,str2 和 str3 引用同一常量池中的字符串}
}
4.5 优点
- 节省内存:相同内容的字符串只存储一个实例,减少内存开销。
- 提高性能:字符串比较操作更高效,因为可以直接比较引用。
4.6 注意事项
- 避免滥用
intern()
- 频繁调用
intern()
可能导致常量池中过多的字符串,增加内存压力和 GC 负担。
- 频繁调用
- 字符串拼接的优化
- 编译时拼接:如
"Hello" + "World"
会在编译时优化为"HelloWorld"
,直接引用常量池中的字符串。 - 运行时拼接:如使用
StringBuilder
进行动态拼接,结果字符串默认不在常量池中,需显式调用intern()
才会被添加。
- 编译时拼接:如
示例代码:
public class StringConcatTest {public static void main(String[] args) {String str1 = "Hello" + "World"; // 编译时优化为 "HelloWorld"String str2 = "HelloWorld";System.out.println(str1 == str2); // 输出 trueString str3 = "Hello";String str4 = str3 + "World"; // 运行时拼接String str5 = "HelloWorld";System.out.println(str4 == str5); // 输出 false}
}
五 常量池的关系与区别
下表总结了静态常量池、运行时常量池、基础类型常量池和字符串常量池的主要区别与联系:
特性 | 静态常量池 | 运行时常量池 | 基础类型常量池 | 字符串常量池 |
---|---|---|---|---|
定义位置 | .class 文件中 | JVM 的方法区(JDK 8 及后为元空间) | 运行时常量池的一部分 | 堆内存中的专用区域 |
生成时间 | 编译时 | 类加载时和运行时 | 编译时和运行时 | 编译时和运行时 |
存储内容 | 字面量、符号引用 | 静态常量池的内容及运行时生成的新常量 | 基本数据类型的常量值及包装类的缓存对象 | 字符串字面量及通过 intern() 添加的字符串 |
是否可变 | 不可变 | 可动态扩展 | 基本类型常量不可变,包装类缓存可复用对象 | 可通过 intern() 动态添加 |
内存管理 | 与类文件生命周期一致 | 与类的生命周期一致,受垃圾回收影响 | 缓存对象存储在堆中,受垃圾回收管理 | 存储在堆中,受垃圾回收管理 |
主要优化目的 | 减少重复字面量和符号引用的存储 | 管理类加载时的常量,支持动态常量的添加 | 减少包装类对象的创建,提升性能 | 减少重复字符串对象的创建,节省内存 |
六 总结
在 Java JVM 中,常量池机制通过不同类型的常量池(静态常量池、运行时常量池、基础类型常量池和字符串常量池)优化了内存使用和性能:
- 静态常量池:在编译时确定,存储类级别的常量和符号引用,存在于每个
.class
文件中。 - 运行时常量池:在类加载后,静态常量池的运行时表示,可以动态添加常量,位于方法区或元空间中。
- 基础类型常量池:针对基本数据类型和其包装类,提供缓存机制,减少对象创建,提高性能。
- 字符串常量池:专门用于存储字符串字面量和通过
intern()
添加的字符串,确保字符串唯一性,节省内存。