》》》》文章末尾免费领取 JVM 学习资料《《《《
JVM的常量池主要有以下几种:
- 运行时常量池
- 字符串常量池
- 基本类型包装类常量池
- class文件常量池
他们之间的关系:
图解说明:
- 每个class的字节码文件中都有一个常量池,里面是编译后即知的该class会用到的字面量与符号引用,这就是class文件常量池。JVM加载class,会将其类信息,包括class文件常量池置于方法区中。
- class类信息及其class文件常量池是字节码的二进制流,它代表的是一个类的静态存储结构,JVM加载类时,需要将其转换为方法区中的java.lang.Class类的对象实例;同时,会将class文件常量池中的内容导入运行时常量池。
- 运行时常量池中的常量对应的内容只是字面量,比如一个"字符串",它还不是String对象;当Java程序在运行时执行到这个"字符串"字面量时,会去字符串常量池里找该字面量的对象引用是否存在,存在则直接返回该引用,不存在则在Java堆里创建该字面量对应的String对象,并将其引用置于字符串常量池中,然后返回该引用。
- Java的基本数据类型中,除了两个浮点数类型(Float、Double),其他的基本数据类型都在各自内部实现了常量池,但都在[-128~127]这个范围内。
class文件常量池
首先我们通过一个简单的类来了解class文件常量池
package com.mazai.learning.jvm;
public class Student {
private final String name = "张三";
private final int entranceAge = 18;
private String eval() {
return eval(String eval() {
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;
}
}
使用反编译命令:javap -verbose Student.class
Classfile /D:/ideaspace/mazai-learning/mazai-jvm/target/classes/com/mazai/learning/jvm/Student.class
Last modified 2022-9-27; size 1581 bytes
MD5 checksum 361fea8d9e4c6966a3a2b295310f3a18
Compiled from "Student.java"
public class com.mazai.learning.jvm.Student
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #19.#53 // java/lang/Object."":()V
#2 = String #54 // 张三
#3 = Fieldref #18.#55 // com/mazai/learning/jvm/Student.name:Ljava/lang/String;
#4 = Fieldref #18.#56 // com/mazai/learning/jvm/Student.entranceAge:I
#5 = String #57 // 优秀
#6 = Fieldref #18.#58 // com/mazai/learning/jvm/Student.eval(I)Ljava/lang/Integer;
#9 = Fieldref #18.#62 // com/mazai/learning/jvm/Student.level:Ljava/lang/Integer;
#10 = String #63 // +
#11 = Class #64 // java/lang/StringBuilder
#12 = Methodref #11.#53 // java/lang/StringBuilder."":()V
#13 = Methodref #11.#65 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#14 = Methodref #11.#66 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#15 = Fieldref #67.#68 // java/lang/System.out:Ljava/io/PrintStream;
#16 = String #69 // base:10
#17 = Methodref #70.#71 // java/io/PrintStream.println:(Ljava/lang/String;)V
#18 = Class #72 // com/mazai/learning/jvm/Student
#19 = Class #73 // 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 eval()V
#32 = Utf8 Code
#33 = Utf8 LineNumberTable
#34 = Utf8 LocalVariableTable
#35 = Utf8 this
#36 = Utf8 Lcom/mazai/learning/jvm/Student;
#37 = Utf8 getEvaluate
#38 = Utf8 ()Ljava/lang/String;
#39 = Utf8 setEvaluate
#40 = Utf8 (Ljava/lang/String;)V
#41 = Utf8 tmp
#42 = Utf8 getScores
#43 = Utf8 ()I
#44 = Utf8 setScores
#45 = Utf8 (I)V
#46 = Utf8 base
#47 = Utf8 getLevel
#48 = Utf8 ()Ljava/lang/Integer;
#49 = Utf8 setLevel
#50 = Utf8 (Ljava/lang/Integer;)V
#51 = Utf8 SourceFile
#52 = Utf8 Student.java
#53 = NameAndType #30:#31 // "":()V
#54 = Utf8 张三
#55 = NameAndType #20:#21 // name:Ljava/lang/String;
#56 = NameAndType #23:#24 // entranceAge:I
#57 = Utf8 优秀
#58 = NameAndType #26:#21 // eval(I)Ljava/lang/Integer;
#62 = NameAndType #28:#29 // level:Ljava/lang/Integer;
#63 = Utf8 +
#64 = Utf8 java/lang/StringBuilder
#65 = NameAndType #77:#78 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#66 = NameAndType #79:#38 // toString:()Ljava/lang/String;
#67 = Class #80 // java/lang/System
#68 = NameAndType #81:#82 // out:Ljava/io/PrintStream;
#69 = Utf8 base:10
#70 = Class #83 // java/io/PrintStream
#71 = NameAndType #84:#40 // println:(Ljava/lang/String;)V
#72 = Utf8 com/mazai/learning/jvm/Student
#73 = Utf8 java/lang/Object
#74 = Utf8 java/lang/Integer
#75 = Utf8 valueOf
#76 = Utf8 (I)Ljava/lang/Integer;
#77 = Utf8 append
#78 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#79 = Utf8 toString
#80 = Utf8 java/lang/System
#81 = Utf8 out
#82 = Utf8 Ljava/io/PrintStream;
#83 = Utf8 java/io/PrintStream
#84 = Utf8 println
{方法信息,此处省略...}
SourceFile: "Student.java"
class文件常量池的内容
class文件常量池存放的是该class编译后即知的,在运行时将会用到的各个“常量”。注意这个常量不是编程中所说的final修饰的变量,而是字面量和符号引用,如下图所示:
这里的#57就是"优秀"的字面量,它不是一个String对象,只是一个使用utf8编码的文本字符串而已。
注意,只有final修饰的成员变量如entranceAge,才会在常量池中存在对应的字面量。而非final的成员变量scores,以及局部变量base(即使使用final修饰了),它们的字面量都不会在常量池中定义。
运行时常量池
JVM在加载某个class的时候,需要完成以下任务:
- 通过该class的全限定名来获取它的二进制字节流,即读取其字节码文件。其内容包括在章节二、class文件常量池中介绍的class文件常量池。
- 将读入的字节流从静态存储结构转换为方法区中的运行时的数据结构。
- 在Java堆中生成该class对应的类对象,代表该class原信息。这个类对象的类型是java.lang.Class,它与普通对象不同的地方在于,普通对象一般都是在new之后创建的,而类对象是在类加载的时候创建的,且是单例。
而上述过程的第二步,就包含了将class文件常量池内容导入运行时常量池。class文件常量池是一个class文件对应一个常量池,而运行时常量池只有一个,多个class文件常量池中的相同字符串只会对应运行时常量池中的一个字符串。
运行时常量池除了导入class文件常量池的内容,还会保存符号引用对应的直接引用(实际内存地址)。这些直接引用是JVM在类加载之后的链接(验证、准备、解析)阶段从符号引用翻译过来的。
此外,运行时常量池具有动态性的特征,它的内容并不是全部来源与编译后的class文件,在运行时也可以通过代码生成常量并放入运行时常量池。比如String.intern()方法。
要注意的是,运行时常量池中保存的“常量”依然是字面量和符号引用。比如字符串,这里放的仍然是单纯的文本字符串,而不是String对象。
String对象到底放在哪里?什么时候创建?下面的章节开始梳理。
字符串常量池
如前所述,class文件常量池和运行时常量池中,都没有直接存储字面量对应的实际对象,比如String对象。那么String对象到底是什么时候在哪里创建的呢?
字面量赋值创建String对象
我们以下面这个简单的例子来说明使用字面量赋值方法来创建一个String对象的大致流程:
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
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);
String::intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的 字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
这个例子也假设相关字符串字面量都是首次被执行到,那么语句3会创建5个对象:两个"a",其中一个的引用被保存在字符串常量池;两个"b",其中一个的引用被保存在字符串常量池;一个"ab",其引用没有被保存在字符串常量池。
两个String对象用"+"拼接
- 如果拼接的都是字符串直接量,则在编译时编译器会将其直接优化为一个完整的字符串,和你直接写一个完整的字符串是一样的。
- 如果拼接的字符串中包含变量,则在编译时编译器采用StringBuilder对其进行优化,即自动创建StringBuilder实例并调用其append()方法,将这些字符串拼接在一起。
语句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。
在JDK1.7前,运行时常量池+字符串常量池是存放在方法区中,HotSpot VM对方法区的实现称为永久代。
在JDK1.7中,字符串常量池从方法区移到堆中,运行时常量池保留在方法区中。
在JDK1.8中,HotSpot移除永久代,使用元空间代替,此时字符串常量池保留在堆中,运行时常量池保留在方法区中,只是实现不一样了,JVM内存变成了直接内存。
字符串常量池是否会被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]
- Boolean : [True, False]
- Character : [0~127]
例如下面的代码,注意其结果:
Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1 == i2);//true
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4);//false
Integer i5 = -128;
Integer i6 = -128;
System.out.println(i5 == i6);//true
Integer i7 = -129;
Integer i8 = -129;
System.out.println(i7 == i8);//false
微信搜索 “Java 码仔”,回复 “jvm” 免费领取 JVM 学习资料