[java]JVM之运行时常量池里到底有什么

2021-05-08  本文已影响0人  dafasoft

1. 概念

首先我们来复习一下java内存模型,java运行时数据区大概分为五块,分别是

而运行时常量池是方法区的一部分,文字解释:

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(ConstantPool Table), 用于存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

从这段描述中我们可以得出结论,运行时常量池里面存放的是从Class文件中的常量池表中加载到的数据,为了搞清楚运行时常量池里有什么,我们需要搞清楚对应常量池表里面有什么

2. 常量池表

2.1 Class文件的数据类型

先说一下Class的文件格式:Class文件的文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型,“无符号数”和“表”。

2.2 常量池

常量池可以比喻为Class文件里的资源仓库,它是Class文件结构中与其他项目关联最多的数据。

常量池中主要存放两大数据:字面量和符号引用。字面量比较接近Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。

2.2.1 符号引用

由于Java代码在进行Javac编译的时候,并不像C/C++那样有“连接”这一步骤,而是在虚拟机加载Clsss文件的时候进行动态连接,因此,在我们将Java代码编译成Class文件后,Class文件并不会保存方法、字段等在内存中的布局。为了解决这个问题,Class文件会在常量池内保存方法、字段等的符号引用。所谓符号引用,我们可以简单的理解为真正内存布局的占位符,在类加载过程的解析阶段,符号引用会被替换为真正的直接引用。

2.2.2 常量池的结构

常量池中每一项常量都是一个表,这些表都有一个共同的特点,即表结构的起始第一位为一个u1类型的标志位,代表着当前常量属于哪一种常量类型

常量池中的项目类型

项目 类型 描述
CONSTANT_Utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 5 长整型字面量
CONSTANT_Double_info 6 双精度浮点型字面量
CONSTANT_Class_info 7 类或接口的符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_Fieldref_info 9 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的部分符号引用
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_MethodType_info 16 表示方法类型
CONSTANT_Dynamic_info 17 表示一个动态计算常量
CONSTANT_InvkoeDynamic_info 18 表示一个动态方法调用点
CONSTANT_Module_info 19 表示一个模块
CONSTANT_Package_info 20 表示一个模块中开放或者导出的包

具体表信息和表结构如下:

2.2.2.1 CONSTANT_Utf8_info

类型 标志 描述
tag u1 值为1
length u2 UTF-8编码的字符串占用的字节数
bytes u1 长度为length的UTF-8编码的字符串,总共length个

2.2.2.2 CONSTANT_Integer_info

项目 类型 描述
tag u1 值为3
bytes u4 按照高位在前存储的int值

2.2.2.3 CONSTANT_Float_info

项目 类型 描述
tag u1 值为4
bytes u4 按照高位在前存储的float值

2.2.2.4 CONSTANT_Long_info

项目 类型 描述
tag u1 值为5
bytes u8 按照高位在前存储的long值

2.2.2.5 CONSTANT_Double_info

项目 类型 描述
tag u1 值为6
bytes u8 按照高位在前存储的double值

2.2.2.6 CONSTANT_Class_info

项目 类型 描述
tag u1 值为7
index u2 指向全限定名常量项的索引

2.2.2.7 CONSTANT_String_info

项目 类型 描述
tag u1 值为8
index u2 指向字符串字面量的索引

2.2.2.8 CONSTANT_Fieldref_info

项目 类型 描述
tag u1 值为9
index u2 指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项
index u2 指向字段描述符CONSTANT_NameAndType的索引项

2.2.2.9 CONSTANT_Methodref_info

项目 类型 描述
tag u1 值为10
index u2 指向声明方法的类或者接口描述符CONSTANT_Class的索引项
index u2 指向名称及类型描述符CONSTANT_NameAndType的索引项

2.2.2.10 CONSTANT_InterfaceMethodref_info

项目 类型 描述
tag u1 值为11
index u2 指向声明方法的接口描述符CONSTANT_Class的索引项
index u2 指向名称及类型描述符CONSTANT_NameAndType的索引项

2.2.2.11 CONSTANT_NameAndType_info

项目 类型 描述
tag u1 值为12
index u2 指向该字段或方法名称常量项的索引
index u2 指向该字段或方法描述符常量项的索引

2.2.2.12 CONSTANT_MethodHandle_info

项目 类型 描述
tag u1 值为15
reference_kind u1 值必须在1至9之间[1-9]它决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为
reference_index u2 值必须是对敞亮吃的有效索引

2.2.2.13 CONSTANT_MethodType_info

项目 类型 描述
tag u1 值为16
descriptor_index u2 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符

2.2.2.14 CONSTANT_Dynamic_info

项目 类型 描述
tag u1 值为17
bootstrap_method_attr_index u2 值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引
name_and_type_index u2 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符

2.2.2.15 CONSTANT_InvkoeDynamic_info

项目 类型 描述
tag u1 值为18
bootstrap_method_attr_index u2 值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引
name_and_type_index u2 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符

2.2.2.16 CONSTANT_Module_info

项目 类型 描述
tag u1 值为19
name_index u2 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示模块名称

2.2.2.17 CONSTANT_Package_info

项目 类型 描述
tag u1 值为19
name_index u2 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示包名称

3. 查看常量池表

如果我们使用文本编辑器打开某个Class文件,那么你见到的场景大概是这样的:


image.png

除了魔数CAFE BABE 其他的信息阅读起来可能会有很大的困难。好在Oracle为我们提供了一个专门用于分析Class文件字节码的工具:javap。javap的使用方式:

javap -verbose xxxx.class

简单写一个java类Test, 代码如下:

public class Test {
    String test = "dafa";
    String test1 = "soft";
    String test3 = "dafasoft";

    public Test() {
    }

    void Test() {
        this.testFun();
    }

    public void testFun() {
    }

    public void testFun1() {
    }

    public void testFun2() {
    }

    public static void main(String[] args) {
    }
}

我们编译后使用javap命令,观察一下它的常量池表是什么样的。它的常量池表截图如下:


image.png

javap工具为我们自动加了注释,但实际上如果我们把上面的表结构看完,不加注释也是看明白的
举两个例子:

3.1. 示例一:String test3 = "dafasoft";

这是我们在java类中定义的字符串字面常量,如何在常量池表中寻找呢?首先,它是一个Field,我们首先寻找Fieldref,上图的表中共有三个Fieldref,我们逐一查找即可。

根据 第2.2.2.8章节的表结构我们知道,Filedref后面的两个值是两个index, 分别指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项指向字段描述符CONSTANT_NameAndType的索引项,比如 #7处的Fieldref对应的值为#9.#40, #9 对应的值为#42, #42对应的值字符串“com/dafasoft/test/Test”; #40对应的值为#14:#12, #14对应的值为字符串test3, #12对应的值为字符串“Ljava/lang/String;”, 这些信息频道一起,我们就可以得出#7处的Fieldref是指向com/dafasoft/test/Test.test3:Ljava/lang/String;处的一个字符引用。

到现在为止,我们已经知道这个类里有一个引用指向com/dafasoft/test/Test.class 的变量test3 ,其类型为String类型,那么它是怎么和它的值"dafasoft"产生关联的呢?这和对象的初始化有关,具体的字节码在<init>方法中:


image.png

注意看Code的第17 和19行。查询字节码指令可知,ldc指令的含义是'将int、float、或String型常量值从常量池中推送至栈顶' putfied指令的含义是'为指定类的实例field赋值',查询常量池表可知,#6对应的值为字符串"dafasoft"。在执行过这两条指令后,我们才完成了对String变量test3的赋值。

3.2 示例二:this.testFun();

这条语句在构造方法Test() 中,要调用方法,需要知道这个方法在方法区的引用。方法的引用在常量池表里的形式为CONSTANT_Methodref_info, 查看本例的常量池表,它的索引为#8,具体的分析形式我们就不展开了,跟示例一的解析方式是一样的,解析完成后我们得知,它是一个指向com/dafasoft/test/Test.testFun:()V的一个符号引用,翻译成java语言就是:com.dafasoft.test包里Test类的void testFun()方法。
调用方式在Test 方法的字节码中:

image.png
注意看Code的第一行,这行指令的意思是,调用指向字符引用 #8的方法,而字符引用 #8 对应的方法就是com/dafasoft/test/Test.testFun:()V

由此我们也可以得出一个结论:当Java类中产生方法调用时才会在常量池中添加该方法的引用。那么本类中定义的方法去哪了?比如,Test类有三个方法,只有testFun出现了调用,testFun1和testFun2的引用就没有出现在常量池表中。那么这三个方法的实现在哪里呢?答案是 字节码的方法表中,当然这是另外的话题了。

常量池表中有17种类型,这17种类型的引用都可以用这种方式推导出来,我们就不一一介绍了。

上一篇 下一篇

猜你喜欢

热点阅读