在 Java 中,类常量池(Class Constant Pool)和运行时常量池(Runtime Constant Pool)是两个密切相关的概念。它们在程序的不同生命周期中发挥作用,尤其是在字节码加载和执行时。具体来说,它们的关系如下:
类常量池(Class Constant Pool)
类常量池是存储在 类文件(.class) 中的常量池,也可以称为编译时常量池。每个类在编译时会生成一个常量池,用于存储类中所有的字面量(如字符串常量、基本类型常量)和符号引用(如类、方法、字段等的引用)。这些常量池的内容在 Java 编译时就已经确定。
- 字面量常量:如
int a = 10;
中的10
,String str = "hello";
中的"hello"
。 - 符号引用:如对方法、字段、类等的引用。
类常量池包含以下内容:
- 字面量常量(例如字符串常量、数字常量)
- 符号引用(对类、方法、字段等的引用)
类常量池存储在每个 .class
文件中。当 JVM 加载该类文件时,常量池会被解析,并且某些常量(特别是字符串常量)会被直接存储在类文件中。
运行时常量池(Runtime Constant Pool)
运行时常量池是在类加载后,JVM 根据 .class
文件中的常量池内容所创建的一个数据结构。它是存在于 JVM 内存中的一个池,包含了在类加载时从类常量池中提取的常量数据,并在运行时进行解析、替换、动态修改等操作。
- 运行时常量池是 JVM 的一部分,位于方法区(在 JDK 8 中,属于元空间,即 Metaspace)中。
- 在程序运行过程中,JVM 会根据类常量池的内容,动态地生成实际的常量池。此时,常量池中存储的是 符号引用 和 字面量值。
运行时常量池的作用是存储程序在运行过程中需要使用的常量,特别是当类加载时遇到的常量,它们会被加载到内存中,供 JVM 在执行时使用。
类常量池与运行时常量池的关系
-
类加载阶段:当 JVM 加载
.class
文件时,类常量池中的内容(字面量和符号引用)会被读取并保存到运行时常量池中。在这个过程中,所有字面量值(例如字符串常量、数字常量)和符号引用(类、方法、字段等的引用)都会被复制到运行时常量池中。 -
符号解析:对于类常量池中的符号引用(如方法引用、字段引用),JVM 会根据实际的类加载顺序,进行符号解析并将其转化为直接引用,这个过程是在类加载后,运行时常量池中完成的。
-
共享与动态性:运行时常量池可以看作是类常量池的一个副本,但它具有更高的灵活性,因为它支持动态处理。在运行时,常量池可以被改变。例如,某些动态生成的字符串、动态加载的类、动态调用的方法等,都会被动态地添加到运行时常量池中。
具体例子
public class Example {
public static void main(String[] args) {
String s = "Hello";
int x = 100;
}
}
编译阶段: 编译器会将 "Hello"
和 100
分别放入类常量池中。字符串 "Hello"
会存储为字面量常量,而 100
会存储为整数常量。
在运行时常量池中,不会立刻被解析成对象,第一次被LDC指令执行时,在“堆”上创建字符串对象,并将对象的引用驻留在字符串常量池中 。
LDC指令:从当前类的常量池加载到操作数栈上。
类加载阶段:当 JVM 加载 Example.class
文件时,类常量池中的 "Hello"
和 100
会被读取到运行时常量池中。对于字符串 "Hello"
,如果这个字符串之前没有被加载过,JVM 会在运行时常量池中创建一个新的字符串对象并引用它。对于数字 100
,这个常量也会被保存在运行时常量池中,但它是基本数据类型的常量。
符号解析阶段:对于类、方法、字段的引用,JVM 会在类加载时通过符号解析的方式,将符号引用转换为直接引用(例如,类的对象或方法的地址等)。例如类符号引用:表示对某个类的引用。Java 字节码中使用类的全限定名(包括包名)作为符号进行引用。例如java.lang.String
类的符号引用。