共计 8597 个字符,预计需要花费 22 分钟才能阅读完成。
在上一篇《JVM 常识梳理之一_JVM 运行时内存区域与 Java 内存模型》中,提到了 JVM 的各种常量池,但没有开展讲述。本文就 JVM 的各种常量池进行一些简略的梳理。
一、常量池概述
JVM 的常量池次要有以下几种:
- class 文件常量池
- 运行时常量池
- 字符串常量池
- 根本类型包装类常量池
它们相互之间关系大抵如下图所示:
- 每个 class 的字节码文件中都有一个常量池,外面是编译后即知的该 class 会用到的
字面量
与符号援用
,这就是class 文件常量池
。JVM 加载 class,会将其类信息,包含 class 文件常量池置于办法区中。 - class 类信息及其 class 文件常量池是字节码的二进制流,它代表的是一个类的动态存储构造,JVM 加载类时,须要将其转换为办法区中的
java.lang.Class
类的对象实例;同时,会将 class 文件常量池中的内容导入运行时常量池
。 - 运行时常量池中的常量对应的内容只是字面量,比方一个 ” 字符串 ”,它还不是 String 对象;当 Java 程序在运行时执行到这个 ” 字符串 ” 字面量时,会去
字符串常量池
里找该字面量的对象援用是否存在,存在则间接返回该援用,不存在则在 Java 堆里创立该字面量对应的 String 对象,并将其援用置于字符串常量池中,而后返回该援用。 - Java 的根本数据类型中,除了两个浮点数类型,其余的根本数据类型都在各自外部实现了常量池,但都在 [-128~127] 这个范畴内。
上面别离对每个常量池具体梳理一下。
二、class 文件常量池
java 的源代码 .java
文件在编译之后会生成 .class
文件,class 文件须要严格遵循 JVM 标准能力被 JVM 失常加载,它是一个二进制字节流文件,外面蕴含了 class 文件常量池的内容。
2.1 查看一个 class 文件内容
jdk 提供了 javap
命令,用于对 class 文件进行反汇编,输入类相干信息。该命令用法如下:
用法: 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> 笼罩疏导类文件的地位
例如,咱们能够编写一个简略的类,如下:
/**
* @author zhaochun
*/
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;}
}
对其进行编译和反汇编:
javac Student.java
javap -v Student.class
# over
失去以下反汇编后果:
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 文件常量池,应用 #
加数字标记每个“常量”。
2.2 class 文件常量池的内容
class 文件常量池寄存的是该 class 编译后即知的,在运行时将会用到的各个“常量”。留神这个常量不是编程中所说的 final
润饰的变量,而是 字面量
和符号援用
,如下图所示:
2.2.1 字面量
字面量大概相当于 Java 代码中的双引号字符串和常量的理论的值,包含:
1. 文本字符串,即代码中用双引号包裹的字符串局部的值。例如刚刚的例子中,有三个字符串:"张三"
,"优良"
,"+"
,它们在 class 文件常量池中别离对应:
#49 = Utf8 张三
#52 = Utf8 优良
#58 = Utf8 +
这里的
#49
就是"张三"
的字面量,它不是一个 String 对象,只是一个应用 utf8 编码的文本字符串而已。
2. 用 final 润饰的成员变量,例如,private static final int entranceAge = 18;
这条语句定义了一个 final 常量entranceAge
,它的值是18
,对应在 class 文件常量池中就有:
#25 = Integer 18
留神,只有 final 润饰的成员变量如
entranceAge
,才会在常量池中存在对应的字面量。而非 final 的成员变量scores
,以及局部变量base
(即便应用 final 润饰了),它们的字面量都不会在常量池中定义。
2.2.2 符号援用
符号援用包含:
1. 类和接口的全限定名,例如:
#11 = Class #59 // java/lang/StringBuilder
#59 = Utf8 java/lang/StringBuilder
2. 办法的名称和描述符,例如:
#38 = Utf8 getScores
#39 = Utf8 ()I
#40 = Utf8 setScores
#41 = Utf8 (I)V
以及这种对其余类的办法的援用:
#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. 字段的名称和描述符,例如:
#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;
以及这种局部变量:
#64 = Utf8 base:10
三、运行时常量池
JVM 在加载某个 class 的时候,须要实现以下工作:
- 通过该 class 的全限定名来获取它的二进制字节流,即读取其字节码文件。其内容包含在章节
二、class 文件常量池
中介绍的 class 文件常量池。 - 将读入的字节流从动态存储构造转换为办法区中的运行时的数据结构。
- 在 Java 堆中生成该 class 对应的类对象,代表该 class 原信息。这个类对象的类型是
java.lang.Class
,它与一般对象不同的中央在于,一般对象个别都是在 new 之后创立的,而类对象是在类加载的时候创立的,且是单例。
而上述过程的第二步,就蕴含了将 class 文件常量池内容导入运行时常量池。class 文件常量池是一个 class 文件对应一个常量池,而运行时常量池只有一个,多个 class 文件常量池中的雷同字符串只会对应运行时常量池中的一个字符串。
运行时常量池除了导入 class 文件常量池的内容,还会保留符号援用对应的间接援用 (理论内存地址)。这些间接援用是 JVM 在类加载之后的链接(验证、筹备、解析) 阶段从符号援用翻译过去的。
此外,运行时常量池具备动态性的特色,它的内容并不是全副起源与编译后的 class 文件,在运行时也能够通过代码生成常量并放入运行时常量池。比方 String.intern()
办法。
String.intern()
办法的剖析见后续章节。
要留神的是,运行时常量池中保留的“常量”仍然是 字面量
和符号援用
。比方字符串,这里放的依然是单纯的文本字符串,而不是 String 对象。
String 对象到底放在哪里?什么时候创立?上面的章节开始梳理。
四、字符串常量池
如前所述,class 文件常量池和运行时常量池中,都没有间接存储字面量对应的理论对象,比方 String 对象。那么 String 对象到底是什么时候在哪里创立的呢?
4.1 字面量赋值创立 String 对象
咱们以上面这个简略的例子来阐明应用字面量赋值办法来创立一个 String 对象的大抵流程:
String s = "黄河之水天上来";
当 Java 虚拟机启动胜利后,下面的字符串 "黄河之水天上来"
的字面量曾经进入运行时常量池;
而后主线程开始运行,第一次执行到这条语句时,JVM 会依据运行时常量池中的这个字面量去 字符串常量池
寻找其中是否有该字面量对应的 String 对象的援用。留神是援用。
如果没找到,就会去 Java 堆创立一个值为 "黄河之水天上来"
的 String 对象,并将该对象的援用保留到 字符串常量池
,而后返回该援用;如果找到了,阐明之前曾经有其余语句通过雷同的字面量赋值创立了该 String 对象,间接返回援用即可。
4.2 字符串常量池
字符串常量池,是 JVM 用来保护字符串实例的一个援用表。在 HotSpot 虚拟机中,它被实现为一个全局的 StringTable,底层是一个 c ++ 的 hashtable。它将字符串的字面量作为 key,理论堆中创立的 String 对象的援用作为 value。
字符串常量池在逻辑上属于办法区,但 JDK1.7 开始,就被挪到了堆区。
String 的字面量被导入 JVM 的运行时常量池时,并不会马上试图在字符串常量池退出对应 String 的援用,而是等到程序理论运行时,要用到这个字面量对应的 String 对象时,才会去字符串常量池试图获取或者退出 String 对象的援用。因而它是懒加载的。
4.3 new String()与 String.intern()
通过上面的例子,能够帮忙咱们加深对字符串常量池的了解。
例 1:
// 语句 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:
// 语句 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。
4.4 字符串常量池是否会被 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]
例如上面的代码,留神其后果:
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);