关于java:JVM知识梳理之二JVM的常量池md

7次阅读

共计 8597 个字符,预计需要花费 22 分钟才能阅读完成。

在上一篇《JVM 常识梳理之一_JVM 运行时内存区域与 Java 内存模型》中,提到了 JVM 的各种常量池,但没有开展讲述。本文就 JVM 的各种常量池进行一些简略的梳理。

一、常量池概述

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 文件常量池的内容。

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 的时候,须要实现以下工作:

  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 对象到底是什么时候在哪里创立的呢?

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);
正文完
 0