常量池概述

JVM 的常量池主要有以下几种:

  • class 文件常量池
  • 运行时常量池
  • 字符串常量池
  • 基本类型包装类常量池

它们相互之间关系大致如下图所示:

  1. 每个 class 的字节码文件中都有一个常量池,里面是编译后即知的该 class 会用到的 字面量符号引用,这就是 class文件常量池。JVM 加载 class,会将其类信息,包括 class 文件常量池置于方法区中。
  2. class 类信息及其 class 文件常量池是字节码的二进制流,它代表的是一个类的静态存储结构,JVM 加载类时,需要将其转换为方法区中的 java.lang.Class 类的对象实例;同时,会将 class 文件常量池中的内容导入 运行时常量池
  3. 运行时常量池中的常量对应的内容只是字面量,比如一个 “字符串”,它还不是 String 对象;当 Java 程序在运行时执行到这个 “字符串” 字面量时,会去 字符串常量池 里找该字面量的对象引用是否存在,存在则直接返回该引用,不存在则在 Java 堆里创建该字面量对应的 String 对象,并将其引用置于字符串常量池中,然后返回该引用。
  4. Java 的基本数据类型中,除了两个浮点数类型,其他的基本数据类型都在各自内部实现了常量池,但都在 [-128~127] 这个范围内。

下面分别对每个常量池具体梳理一下。

class 文件常量池

java 的源代码 .java 文件在编译之后会生成 .class 文件,class 文件需要严格遵循 JVM 规范才能被 JVM 正常加载,它是一个二进制字节流文件,里面包含了 class 文件常量池的内容。

查看一个 class 文件内容

jdk 提供了 javap 命令,用于对 class 文件进行反汇编,输出类相关信息。该命令用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
用法: javap <options> <classes>
其中, 可能的选项包括:
-help --help -? 输出此用法消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
-classpath <path> 指定查找用户类文件的位置
-cp <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置

例如,我们可以编写一个简单的类,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Student {
private final String name = "张三";
private final int entranceAge = 18;
private String evaluate = "优秀";
private int scores = 95;
private Integer level = 5;

public String getEvaluate() {
return evaluate;
}

public void setEvaluate(String evaluate) {
String tmp = "+";
this.evaluate = evaluate + tmp;
}

public int getScores() {
return scores;
}

public void setScores(int scores) {
final int base = 10;
System.out.println("base:" + base);
this.scores = scores + base;
}

public Integer getLevel() {
return level;
}

public void setLevel(Integer level) {
this.level = level;
}
}

对其进行编译和反汇编:

1
2
3
4
5
javac Student.java

javap -v Student.class

# over

得到以下反汇编结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
Classfile /home/work/sources/open_projects/lib-zc-crypto/src/test/java/Student.class
Last modified 2021-1-4; size 1299 bytes
MD5 checksum 06dfdad9da59e2a64d62061637380969
Compiled from "Student.java"
public class Student
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #19.#48 // java/lang/Object."<init>":()V
#2 = String #49 // 张三
#3 = Fieldref #18.#50 // Student.name:Ljava/lang/String;
#4 = Fieldref #18.#51 // Student.entranceAge:I
#5 = String #52 // 优秀
#6 = Fieldref #18.#53 // Student.evaluate:Ljava/lang/String;
#7 = Fieldref #18.#54 // Student.scores:I
#8 = Methodref #55.#56 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#9 = Fieldref #18.#57 // Student.level:Ljava/lang/Integer;
#10 = String #58 // +
#11 = Class #59 // java/lang/StringBuilder
#12 = Methodref #11.#48 // java/lang/StringBuilder."<init>":()V
#13 = Methodref #11.#60 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#14 = Methodref #11.#61 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#15 = Fieldref #62.#63 // java/lang/System.out:Ljava/io/PrintStream;
#16 = String #64 // base:10
#17 = Methodref #65.#66 // java/io/PrintStream.println:(Ljava/lang/String;)V
#18 = Class #67 // Student
#19 = Class #68 // java/lang/Object
#20 = Utf8 name
#21 = Utf8 Ljava/lang/String;
#22 = Utf8 ConstantValue
#23 = Utf8 entranceAge
#24 = Utf8 I
#25 = Integer 18
#26 = Utf8 evaluate
#27 = Utf8 scores
#28 = Utf8 level
#29 = Utf8 Ljava/lang/Integer;
#30 = Utf8 <init>
#31 = Utf8 ()V
#32 = Utf8 Code
#33 = Utf8 LineNumberTable
#34 = Utf8 getEvaluate
#35 = Utf8 ()Ljava/lang/String;
#36 = Utf8 setEvaluate
#37 = Utf8 (Ljava/lang/String;)V
#38 = Utf8 getScores
#39 = Utf8 ()I
#40 = Utf8 setScores
#41 = Utf8 (I)V
#42 = Utf8 getLevel
#43 = Utf8 ()Ljava/lang/Integer;
#44 = Utf8 setLevel
#45 = Utf8 (Ljava/lang/Integer;)V
#46 = Utf8 SourceFile
#47 = Utf8 Student.java
#48 = NameAndType #30:#31 // "<init>":()V
#49 = Utf8 张三
#50 = NameAndType #20:#21 // name:Ljava/lang/String;
#51 = NameAndType #23:#24 // entranceAge:I
#52 = Utf8 优秀
#53 = NameAndType #26:#21 // evaluate:Ljava/lang/String;
#54 = NameAndType #27:#24 // scores:I
#55 = Class #69 // java/lang/Integer
#56 = NameAndType #70:#71 // valueOf:(I)Ljava/lang/Integer;
#57 = NameAndType #28:#29 // level:Ljava/lang/Integer;
#58 = Utf8 +
#59 = Utf8 java/lang/StringBuilder
#60 = NameAndType #72:#73 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#61 = NameAndType #74:#35 // toString:()Ljava/lang/String;
#62 = Class #75 // java/lang/System
#63 = NameAndType #76:#77 // out:Ljava/io/PrintStream;
#64 = Utf8 base:10
#65 = Class #78 // java/io/PrintStream
#66 = NameAndType #79:#37 // println:(Ljava/lang/String;)V
#67 = Utf8 Student
#68 = Utf8 java/lang/Object
#69 = Utf8 java/lang/Integer
#70 = Utf8 valueOf
#71 = Utf8 (I)Ljava/lang/Integer;
#72 = Utf8 append
#73 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#74 = Utf8 toString
#75 = Utf8 java/lang/System
#76 = Utf8 out
#77 = Utf8 Ljava/io/PrintStream;
#78 = Utf8 java/io/PrintStream
#79 = Utf8 println
{
方法信息,此处省略...
}
SourceFile: "Student.java"

其中的 Constant pool 就是 class 文件常量池,使用 # 加数字标记每个“常量”。

class 文件常量池的内容

class 文件常量池存放的是该 class 编译后即知的,在运行时将会用到的各个“常量”。注意这个常量不是编程中所说的 final 修饰的变量,而是 字面量符号引用,如下图所示:

字面量

字面量大约相当于 Java 代码中的双引号字符串和常量的实际的值,包括:

1.文本字符串,即代码中用双引号包裹的字符串部分的值。例如刚刚的例子中,有三个字符串:"张三""优秀""+",它们在 class 文件常量池中分别对应:

1
2
3
#49 = Utf8               张三
#52 = Utf8 优秀
#58 = Utf8 +

这里的 #49 就是 "张三" 的字面量,它不是一个 String 对象,只是一个使用 utf8 编码的文本字符串而已。

2.用 final 修饰的成员变量,例如,private static final int entranceAge = 18; 这条语句定义了一个 final 常量 entranceAge,它的值是 18,对应在 class 文件常量池中就有:

1
#25 = Integer            18

注意,只有 final 修饰的成员变量如 entranceAge,才会在常量池中存在对应的字面量。而非 final 的成员变量 scores,以及局部变量 base(即使使用 final 修饰了),它们的字面量都不会在常量池中定义。

符号引用

符号引用包括:

1.类和接口的全限定名,例如:

1
2
#11 = Class              #59            // java/lang/StringBuilder
#59 = Utf8 java/lang/StringBuilder

2.方法的名称和描述符,例如:

1
2
3
4
5
#38 = Utf8               getScores
#39 = Utf8 ()I

#40 = Utf8 setScores
#41 = Utf8 (I)V

以及这种对其他类的方法的引用:

1
2
3
4
5
6
7
8
#8 = Methodref          #55.#56        // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;

#55 = Class #69 // java/lang/Integer
#69 = Utf8 java/lang/Integer

#56 = NameAndType #70:#71 // valueOf:(I)Ljava/lang/Integer;
#70 = Utf8 valueOf
#71 = Utf8 (I)Ljava/lang/Integer;

3.字段的名称和描述符,例如:

1
2
3
4
5
6
7
8
#3 = Fieldref           #18.#50        // Student.name:Ljava/lang/String;

#18 = Class #67 // Student
#67 = Utf8 Student

#50 = NameAndType #20:#21 // name:Ljava/lang/String;
#20 = Utf8 name
#21 = Utf8 Ljava/lang/String;

以及这种局部变量:

1
#64 = Utf8               base:10

运行时常量池

JVM 在加载某个 class 的时候,需要完成以下任务:

  1. 通过该 class 的全限定名来获取它的二进制字节流,即读取其字节码文件。其内容包括 class文件常量池 中介绍的 class 文件常量池。
  2. 将读入的字节流从静态存储结构转换为方法区中的运行时的数据结构。
  3. 在 Java 堆中生成该 class 对应的类对象,代表该 class 原信息。这个类对象的类型是 java.lang.Class,它与普通对象不同的地方在于,普通对象一般都是在 new 之后创建的,而类对象是在类加载的时候创建的,且是单例。

而上述过程的第二步,就包含了将 class 文件常量池内容导入运行时常量池。class 文件常量池是一个 class 文件对应一个常量池,而运行时常量池只有一个,多个 class 文件常量池中的相同字符串只会对应运行时常量池中的一个字符串。

运行时常量池除了导入 class 文件常量池的内容,还会保存符号引用对应的直接引用(实际内存地址)。这些直接引用是 JVM 在类加载之后的链接(验证、准备、解析)阶段从符号引用翻译过来的。

此外,运行时常量池具有动态性的特征,它的内容并不是全部来源与编译后的 class 文件,在运行时也可以通过代码生成常量并放入运行时常量池。比如 String.intern() 方法。

String.intern() 方法的分析见后续章节。

要注意的是,运行时常量池中保存的“常量”依然是 字面量符号引用。比如字符串,这里放的仍然是单纯的文本字符串,而不是 String 对象。

String 对象到底放在哪里?什么时候创建?下面的章节开始梳理。

字符串常量池

如前所述,class 文件常量池和运行时常量池中,都没有直接存储字面量对应的实际对象,比如 String 对象。那么 String 对象到底是什么时候在哪里创建的呢?

字面量赋值创建 String 对象

我们以下面这个简单的例子来说明使用字面量赋值方法来创建一个 String 对象的大致流程:

1
String s = "黄河之水天上来";

当 Java 虚拟机启动成功后,上面的字符串 "黄河之水天上来" 的字面量已经进入运行时常量池;

然后主线程开始运行,第一次执行到这条语句时,JVM 会根据运行时常量池中的这个字面量去 字符串常量池 寻找其中是否有该字面量对应的 String 对象的引用。注意是引用。

如果没找到,就会去 Java 堆创建一个值为 "黄河之水天上来" 的 String 对象,并将该对象的引用保存到 字符串常量池,然后返回该引用;如果找到了,说明之前已经有其他语句通过相同的字面量赋值创建了该 String 对象,直接返回引用即可。

字符串常量池

字符串常量池,是 JVM 用来维护字符串实例的一个引用表。在 HotSpot 虚拟机中,它被实现为一个全局的 StringTable,底层是一个 c++的 hashtable。它将字符串的字面量作为 key,实际堆中创建的 String 对象的引用作为 value。

字符串常量池在逻辑上属于方法区,但 JDK1.7 开始,就被挪到了堆区。

String 的字面量被导入 JVM 的运行时常量池时,并不会马上试图在字符串常量池加入对应 String 的引用,而是等到程序实际运行时,要用到这个字面量对应的 String 对象时,才会去字符串常量池试图获取或者加入 String 对象的引用。因此它是懒加载的。

new String()与 String.intern()

通过下面的例子,可以帮助我们加深对字符串常量池的理解。

例 1:

1
2
3
4
5
// 语句1
String s1 = new String("asdf");

// 语句2
System.out.println(s1 == "asdf");

这个例子中假设 "asdf" 是首次被执行,那么语句 1 会创建两个 String 对象。一个是 JVM 拿字面量 "asdf" 去字符串常量池试图获取其对应 String 对象的引用,因为是首次执行,所以没找到,于是在堆中创建了一个 "asdf" 的 String 对象,并将其引用保存到字符串常量池中,然后返回;返回之后,因为 new 的存在,JVM 又在堆中创建了与 "asdf" 等值的另一个 String 对象。因此这条语句创建了两个 String 对象,它们值相等,都是 "asdf",但是引用(内存地址)不同,所以语句 2 返回 false。

例 2:

1
2
3
4
5
6
7
8
9
10
11
// 语句3
String s3 = new String("a") + new String("b");

// 语句4
s3.intern();

// 语句5
String s4 = "ab";

// 语句6
System.out.println(s3 == s4);

这个例子也假设相关字符串字面量都是首次被执行到,那么语句 3 会创建 5 个对象:两个 “a”,其中一个的引用被保存在字符串常量池;两个 “b”,其中一个的引用被保存在字符串常量池;一个 “ab”,其引用没有被保存在字符串常量池。

两个 String 对象用 “+” 拼接会被优化为 StringBuffer 的 append 拼接,然后 toString 方法,与 new 一样会直接在堆中创建对象。

语句 4 要注意,JDK1.6 和 JDK1.7 开始,String.intern()的执行逻辑是不一样的。

  • JDK1.6 会判断 “ab” 在字符串常量池中不存在,于是创建新的 “ab” 对象并将其引用保存到字符串常量池。
  • JDK1.7 开始,判断 “ab” 在字符串常量池里不存在的话,会直接把 s3 的引用保存到字符串常量池。

因此对于语句 6,如果是 JDK1.6 及以前的版本,结果就是 false;而如果是 JDK1.7 开始的版本,结果就是 true。

如果没有语句 4,那么语句 6 结果一定是 false。

字符串常量池是否会被 GC

字符串常量池本身不会被 GC,但其中保存的引用所指向的 String 对象们是可以被回收的。否则字符串常量池总是 “只进不出”,那么很可能会导致内存泄露。

在 HotSpot 的字符串常量池实现 StringTable 中,提供了相应的接口用于支持 GC,不同的 GC 策略会在适当的时候调用它们。一般实在 Full GC 的时候,额外调用 StringTable 的对应接口做可达性分析,将不可达的 String 对象的引用从 StringTable 中移除掉并销毁其指向的 String 对象。

封装类常量池

除了字符串常量池,Java 的基本类型的封装类大部分也都实现了常量池。包括 Byte,Short,Integer,Long,Character,Boolean,注意,浮点数据类型 Float,Double 是没有常量池的。

封装类的常量池是在各自内部类中实现的,比如 IntegerCache(Integer 的内部类),自然也位于堆区。

要注意的是,这些常量池是有范围的:

  • Byte, Short, Integer, Long : [-128~127]
  • Character : [0~127]
  • Boolean : [True, False]

例如下面的代码,注意其结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1 == i2);

Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4);

Integer i5 = -128;
Integer i6 = -128;
System.out.println(i5 == i6);

Integer i7 = -129;
Integer i8 = -129;
System.out.println(i7 == i8);