大家好,我是大明哥,一个专注「死磕 Java」系列创作的硬核程序员。 本文已收录到我的技术网站:skjava.com。有全网最优质的系列文章、Java 全栈技术文档以及大厂完整面经
回答
常量池是 JVM 的一部分,主要用于存储类和接口中的编译时常量以及运行时被解析的引用。它有如下两种:
- class 文件常量池
class 文件常量池是指在 Java 代码编译成字节码文件(.class文件)时,JVM 会将所有的字面量(如文本字符串、定义为final的常量值)和符号引用(如类和接口的全路径名、字段的名称和描述符、方法的名称和描述符等)存储在.class文件的常量池中。
- 运行时常量池
当类和接口被加载到 JVM 时,class 文件常量池中的数据会被加载到运行时常量池中。运行时常量池位于 JVM 的方法区。在这个过程中,JVM会对这些常量和符号引用进行解析,将它们转换成直接引用。除此之外,运行时常量池还可以在 Java 运行时动态地将新的常量放入池中,比如 String 的 intern()
。
扩展
JVM 中其实有四种常量池:
- Class文件常量池
- 运行时常量池
- 字符串常量池
- 封装类常量池
Class文件常量池
Class文件常量池也是编译时常量池,它是Java类文件结构的一部分。当 Java 源代码文件编译成字节码文件(.class文件)时,Class文件常量池就生成的。在编译源代码到字节码的过程中,编译器会收集该 Java 文件中所有的字面量(如整数、浮点数、字符串等)和符号引用(如类和接口的全限定名、字段和方法的名称及描述符等)并将它们放入Class文件的常量池中。
Class 文件常量池主要包括字面量和符号引用。为了更加直观地看清楚 Class 文件常量池的内容,我们编写一个简单的类:
public class ClassConstantTest {
private final String name = "大明哥";
private final int age = 18;
private String sk = "skjava.com";
private int salary = 10000000;
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String getSk() {
return sk;
}
public int getSalary() {
return salary;
}
}
先通过 javac
命令编译该 Java 文件,然后在通过 javac -v
查看 class 文件信息:
Classfile /Users/chenssy/Documents/workSpace/project-space/skjava-demo/src/main/java/com/skjava/demo/skjavademo/java/jichu/ClassConstantTest.class
Last modified 2024-2-26; size 719 bytes
MD5 checksum db91b97f7827c1dcdbd6aab863188316
Compiled from "ClassConstantTest.java"
public class com.skjava.demo.skjavademo.java.jichu.ClassConstantTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #10.#31 // java/lang/Object."<init>":()V
#2 = String #32 // 大明哥
#3 = Fieldref #9.#33 // com/skjava/demo/skjavademo/java/jichu/ClassConstantTest.name:Ljava/lang/String;
#4 = Fieldref #9.#34 // com/skjava/demo/skjavademo/java/jichu/ClassConstantTest.age:I
#5 = String #35 // skjava.com
#6 = Fieldref #9.#36 // com/skjava/demo/skjavademo/java/jichu/ClassConstantTest.sk:Ljava/lang/String;
#7 = Integer 10000000
#8 = Fieldref #9.#37 // com/skjava/demo/skjavademo/java/jichu/ClassConstantTest.salary:I
#9 = Class #38 // com/skjava/demo/skjavademo/java/jichu/ClassConstantTest
#10 = Class #39 // java/lang/Object
#11 = Utf8 name
#12 = Utf8 Ljava/lang/String;
#13 = Utf8 ConstantValue
#14 = Utf8 age
#15 = Utf8 I
#16 = Integer 18
#17 = Utf8 sk
#18 = Utf8 salary
#19 = Utf8 <init>
#20 = Utf8 ()V
#21 = Utf8 Code
#22 = Utf8 LineNumberTable
#23 = Utf8 getName
#24 = Utf8 ()Ljava/lang/String;
#25 = Utf8 getAge
#26 = Utf8 ()I
#27 = Utf8 getSk
#28 = Utf8 getSalary
#29 = Utf8 SourceFile
#30 = Utf8 ClassConstantTest.java
#31 = NameAndType #19:#20 // "<init>":()V
#32 = Utf8 大明哥
#33 = NameAndType #11:#12 // name:Ljava/lang/String;
#34 = NameAndType #14:#15 // age:I
#35 = Utf8 skjava.com
#36 = NameAndType #17:#12 // sk:Ljava/lang/String;
#37 = NameAndType #18:#15 // salary:I
#38 = Utf8 com/skjava/demo/skjavademo/java/jichu/ClassConstantTest
#39 = Utf8 java/lang/Object
{
public com.skjava.demo.skjavademo.java.jichu.ClassConstantTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String 大明哥
7: putfield #3 // Field name:Ljava/lang/String;
10: aload_0
11: bipush 18
13: putfield #4 // Field age:I
16: aload_0
17: ldc #5 // String skjava.com
19: putfield #6 // Field sk:Ljava/lang/String;
22: aload_0
23: ldc #7 // int 10000000
25: putfield #8 // Field salary:I
28: return
LineNumberTable:
line 3: 0
line 4: 4
line 5: 10
line 6: 16
line 7: 22
public java.lang.String getName();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: ldc #2 // String 大明哥
2: areturn
LineNumberTable:
line 11: 0
public int getAge();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: bipush 18
2: ireturn
LineNumberTable:
line 15: 0
public java.lang.String getSk();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #6 // Field sk:Ljava/lang/String;
4: areturn
LineNumberTable:
line 19: 0
public int getSalary();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #8 // Field salary:I
4: ireturn
LineNumberTable:
line 23: 0
}
这了的 Constant pool
就是常量池了
字面量
什么是字面量?字面量是在源代码中直接表示值的符号或组合,它可以理解为实际的值,通过源代码直接给出的值。Java 支持八种类型的字面量:int
、long
、float
、double
、boolean
、char
、String
、null
。
- 文本字符串
代码中用双引号(" "
)包裹的字符串部分的值,如下:
#2 = String #32 // 大明哥
#32 = Utf8 大明哥
#5 = String #35 // skjava.com
#35 = Utf8 skjava.com
- 用 final 修饰的成员变量
如上面的:private final int age = 18;
,对应常量池:
#16 = Integer 18
这里有两点需要注意:
- 保存在常量池中的是值,也就是上面的
"大明哥"
和"18"
。 - 对于非
final
的成员,它们的字面量都不会在常量池中定义。
符号引用
符号引用主要包括:类和接口的全限定名、字段的名称和描述符、方法中的名称和描述符。
- 类和接口的全限定名
#9 = Class #38 // com/skjava/demo/skjavademo/java/jichu/ClassConstantTest
#38 = Utf8 com/skjava/demo/skjavademo/java/jichu/ClassConstantTest
- 字段的名称和描述符
#3 = Fieldref #9.#33 // com/skjava/demo/skjavademo/java/jichu/ClassConstantTest.name:Ljava/lang/String;
#4 = Fieldref #9.#34 // com/skjava/demo/skjavademo/java/jichu/ClassConstantTest.age:I
- 方法中的名称和描述符
即参数类型和返回值
#23 = Utf8 getName
#24 = Utf8 ()Ljava/lang/String;
#25 = Utf8 getAge
#26 = Utf8 ()I
运行时常量池
运行时常量池是方法区的一部分。在Java程序运行期间,每个已加载的类都会有一个对应的运行时常量池,用于存储类中的常量、静态变量以及符号引用。它的主要作用是提供了一种在程序运行时动态访问常量的机制。
它与 Class 文件常量池不同,Class 文件常量池是静态的,我们也可以称之为静态常量池,它是在编译期间生成的,并且存储在类文件中。而运行时常量池是在类加载过程中创建创建的,并存储在 JVM 的方法区中的。
运行时常量池包含了在类加载过程中从类文件中加载的常量信息,包括字面常量和符号引用。
- 对于字面常量,它们的值会被直接存储在运行时常量池中。
- 对于符号引用,JVM会将符号引用转换为实际的内存地址或方法指针,并存储在运行时常量池中。
除了这些,在程序运行时,我们还可以通过程序动态生成一些常量假如其中,例如:String#intern()
。关于 ntern()
请阅读这篇文章:String 的intern() 原理是怎样的?
字符串常量池
字符串常量池也称之为全局字符串常量池,整个 JVM 就一个。它的主要目的是用于存储字符串常量。
由于字符串的不可变性,使得字符串常量池的实现成为了可能。它会确保相同的字符串常量在内存中只存在一份,即相同的字符串常量只会在常量池中存储一次。
关于字符串常量池推荐看这几篇文章:
String str = new String("abc");创建了几个对象,为什么? String 的intern() 原理是怎样的? String a = "ab"; String b = "a" + "b"; a == b 吗?
关于字符串常量池,大明哥这里补充几点:
- 运行时常量池在方法区,JDK 6 ,字符串常量池在永久代,而到JDK 7后,字符串常量池被移到了Java 堆去了。
- 字符串常量池是 JVM 所维护的一个字符串实例的引用表,在 HotSpot VM中,它是一个叫做
StringTable
的全局表。在字符串常量池中维护的是字符串实例的引用,底层C++实现就是一个Hashtable
。
封装类常量池
在 Java 中,大部分的基本类型的封装类都实现了常量池。包括Byte
、Short
、Integer
、Long
、Character
、Boolean
,但是浮点类型Float
、Double
是没有常量池的,个人认为是因为浮点数的精度问题导致他们没有必要使用常量池。
Byte
、Short
、Integer
、Long
:会存储-128
到127
之间的所有常量值。Character
:存储0
到127
之间的所有常量值,包括所有ASCII字符。Boolean
:会缓存true
和false
这两个常量值。
关于封装类常量池推荐看这篇文章:Integer a1 = 100 Integer a2 = 100 , a1 == a2? 原因是什么?