在上一篇《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.javajavap -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_SUPERConstant 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:
// 语句1String s1 = new String("asdf");// 语句2System.out.println(s1 == "asdf");
这个例子中假如"asdf"
是首次被执行,那么语句1会创立两个String对象。一个是JVM拿字面量"asdf"
去字符串常量池试图获取其对应String对象的援用,因为是首次执行,所以没找到,于是在堆中创立了一个"asdf"
的String对象,并将其援用保留到字符串常量池中,而后返回;返回之后,因为new
的存在,JVM又在堆中创立了与"asdf"
等值的另一个String对象。因而这条语句创立了两个String对象,它们值相等,都是"asdf"
,然而援用(内存地址)不同,所以语句2返回false。
例2:
// 语句3String s3 = new String("a") + new String("b");// 语句4s3.intern();// 语句5String s4 = "ab";// 语句6System.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);