文章目录
- 前言
- 常量池(Constant Pool)
- 运行时常量池(Runtime Constant Pool)
- 字符串常量池(String Literal Pool)
- 运行时常量池和字符串常量池位置变化
- 方法区与永久代和元空间的关系
- 三者之间的关系
- 常量池与运行时常量池
- 运行时常量池与字符串常量池
- 总结
前言
在Java虚拟机(JVM)中,常量池、运行时常量池和字符串常量池是三个相关但又有所区别的概念。下面详细解释这三个概念及其相互的联系。
常量池(Constant Pool)
常量池也被称为 Class 文件常量池,是指每个 Java 类编译后的 .class 文件中的一个表结构,每个类文件(.class 文件)都有一个独立的常量池(Constant Pool)。
它存储了类或接口在编译时已知的各种字面量和符号引用。这些信息包括但不限于:
- 字面量:如整数、浮点数、字符串等。
- 符号引用:如类和接口的全限定名、字段名称和描述符、方法名称和描述符等。这些符号引用在类加载过程中会被解析为直接引用,从而让 JVM 能够准确地定位和调用相关的类、字段和方法。
每个类或接口都有其对应的常量池,它是类加载过程中不可或缺的一部分,用于解析类中的各种引用。
特点:常量池是静态的,在编译阶段就已经确定,存储在 .class 文件中,它是 JVM 后续加载和使用类的重要数据基础。
代码
java">public class HelloWorld {public static void main(String[] args) {System.out.println("hello world");}
}
通过反编译查看以上代码的.class文件
输入javap -v HelloWorld.class查看类文件的常量池内容:
Classfile /D:/Test/jvm/out/production/jvm/cn/qf/HelloWorld.classLast modified 2024-12-6; size 567 bytesMD5 checksum 8efebdac91aa496515fa1c161184e354Compiled from "HelloWorld.java"
public class cn.qf.HelloWorldminor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 = Methodref #6.#20 // java/lang/Object."<init>":()V#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;#3 = String #23 // hello world#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V#5 = Class #26 // cn/qf//HelloWorld#6 = Class #27 // java/lang/Object#7 = Utf8 <init>#8 = Utf8 ()V#9 = Utf8 Code#10 = Utf8 LineNumberTable#11 = Utf8 LocalVariableTable#12 = Utf8 this#13 = Utf8 Lcn/qf//HelloWorld;#14 = Utf8 main#15 = Utf8 ([Ljava/lang/String;)V#16 = Utf8 args#17 = Utf8 [Ljava/lang/String;#18 = Utf8 SourceFile#19 = Utf8 HelloWorld.java#20 = NameAndType #7:#8 // "<init>":()V#21 = Class #28 // java/lang/System#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;#23 = Utf8 hello world#24 = Class #31 // java/io/PrintStream#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V#26 = Utf8 cn/qf/HelloWorld#27 = Utf8 java/lang/Object#28 = Utf8 java/lang/System#29 = Utf8 out#30 = Utf8 Ljava/io/PrintStream;#31 = Utf8 java/io/PrintStream#32 = Utf8 println#33 = Utf8 (Ljava/lang/String;)V
{public cn.qf.HelloWorld();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 4: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcn/qf/HelloWorld;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #3 // String hello world5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 6: 0line 7: 8LocalVariableTable:Start Length Slot Name Signature0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
重点观察Constant pool
运行时常量池(Runtime Constant Pool)
运行时常量池是每一个类或接口的常量池表的一个运行时表示形式,运行时常量池是方法区的一部分。
在Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference)的 常量池表(Constant Pool Table) 。
当JVM加载一个类文件时,会将该类的常量池信息(常量池表)加载到方法区内的运行时常量池中。
字符串常量池(String Literal Pool)
字符串常量池是专门用于存放程序中使用String字面量创建的字符串对象的一个特殊区域。
在JDK 6及之前,字符串常量池是运行时常量池的一部分,而JDK 7及之后字符串常量池被移到堆中,因此字符串常量池不再是严格意义上的运行时常量池的一部分。
字符串常量池特别针对字符串进行了优化设计,以实现字符串共享,减少内存占用。
它是一个类似于哈希表(HashTable)的数据结构,在 HotSpot 虚拟机中由StringTable 类实现。
- 作用:节省内存空间,提高性能。
- 位置:与运行时常量池类似,在JDK 7之前位于永久代,在JDK 7及之后被移到了堆中。
运行时常量池和字符串常量池位置变化
JDK1.7 之前,方法区的具体实现是永久代(Permanent Generation),运行时常量池和字符串常量池存放在方法区(永久代)。
JDK 7 开始,字符串常量池和静态变量从永久代中移动到了 Java 堆中,运行时常量池不变。方法区仍然由永久代实现。
这一改变主要是为了避免永久代的内存溢出问题,因为永久代的空间相对较小,且难以进行调优,而堆的管理相对更加灵活。
JDK 8 及以后的版本中,永久代被元空间(Metaspace)所取代(运行时常量池还是在方法区中),但字符串常量池仍然在堆中。
元空间使用的是本地内存,不再受 JVM 堆内存的限制。方法区由元空间实现,它主要存储类的元数据信息,而字符串常量池独立于元空间,在堆中进行管理。
方法区与永久代和元空间的关系
方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。
并且,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。
三者之间的关系
常量池与运行时常量池
常量池是 .class 文件中的静态数据结构,而运行时常量池是常量池在运行时的内存表示。
当类被加载到 JVM 中时,JVM 会将 .class 文件中的常量池信息复制到运行时常量池中,并且对符号引用进行解析和转换。
可以说,运行时常量池是常量池在运行时的具体体现,二者是静态与动态的关系。
运行时常量池与字符串常量池
- 运行时常量池包含了多种类型的常量,而字符串常量池专门用于存储字符串常量。
- 字符串常量池利用运行时常量池的框架,实现了字符串的共享和复用,以节省内存。
- 在JDK 6及之前,字符串常量池是运行时常量池的一部分,而JDK 7及之后字符串常量池被移到堆中,因此字符串常量池不再是严格意义上的运行时常量池的一部分。
- 运行时常量池中存储的是字符串常量的引用,而不是字符串对象本身。字符串对象的实际内容存储在堆中的字符串常量池。
- 例如,当一个类文件中定义了字符串常量 “hello” 时:
- 运行时常量池中会存储一个指向堆中字符串常量池的引用。
- 堆中的字符串常量池会存储实际的 String 对象。
例如:
java">String str1 = "hello";
String str2 = "hello";
在 JDK 7 及以后,当第一次遇到 “hello” 时,会在堆中的字符串常量池创建 “hello” 字符串对象,运行时常量池记录其引用;
第二次遇到 “hello” 时,发现字符串常量池已有该对象,运行时常量池直接复用这个引用,所以 str1 和 str2 引用的是同一个堆中的字符串对象。
总结
- 常量池是.class文件的一部分(每个class文件都有自己独立的常量池),提供了类或接口在编译时所需的各种常量信息。
- 运行时常量池是JVM方法区中的一部分,当JVM加载一个类文件时,会将该类的常量池信息(常量池表)加载到方法区内的运行时常量池中。
- 字符串常量池特别针对字符串进行了优化设计,以实现字符串共享,减少内存占用。它是一个类似于哈希表(HashTable)的数据结构,在 HotSpot 虚拟机中由 StringTable 类实现。
- JDK 1.7之后运行时常量池在方法区中,字符串常量池在堆中。因此在JDK 6及之前,字符串常量池是运行时常量池的一部分,而JDK 7及之后字符串常量池被移到堆中,因此字符串常量池不再是严格意义上的运行时常量池的一部分。
- 运行时常量池中存储的是字符串常量的引用,而不是字符串对象本身。字符串对象的实际内容存储在堆中的字符串常量池。