1. 概念
首先我们来复习一下java内存模型,java运行时数据区大概分为五块,分别是
- 方法区
- 虚拟机栈
- 本地方法栈
- 堆
- 程序计数器
而运行时常量池是方法区的一部分,文字解释:
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(ConstantPool Table), 用于存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
从这段描述中我们可以得出结论,运行时常量池里面存放的是从Class文件中的常量池表中加载到的数据,为了搞清楚运行时常量池里有什么,我们需要搞清楚对应常量池表里面有什么
2. 常量池表
2.1 Class文件的数据类型
先说一下Class的文件格式:Class文件的文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型,“无符号数”和“表”。
- 无符号数属于基本的数据类型,以u1/u2/u4/u8来分别代表1个字节、2个字节、4个字节、8个字节的的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按utf-8编码构成字符串值。
- 表是由多个无符号数或者其他表作为数据项构成的符合数据类型,为了便于区分,所有表的命名都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视作是一张表。
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文件,那么你见到的场景大概是这样的:
除了魔数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命令,观察一下它的常量池表是什么样的。它的常量池表截图如下:
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>方法中:
注意看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 方法的字节码中:
注意看Code的第一行,这行指令的意思是,调用指向字符引用 #8的方法,而字符引用 #8 对应的方法就是
com/dafasoft/test/Test.testFun:()V
由此我们也可以得出一个结论:当Java类中产生方法调用时才会在常量池中添加该方法的引用。那么本类中定义的方法去哪了?比如,Test类有三个方法,只有testFun出现了调用,testFun1和testFun2的引用就没有出现在常量池表中。那么这三个方法的实现在哪里呢?答案是 字节码的方法表中,当然这是另外的话题了。
常量池表中有17种类型,这17种类型的引用都可以用这种方式推导出来,我们就不一一介绍了。