1 前言
本文主要从编译后的Java语言的字节码类文件格式与结构方面论述JVM常量池的相关特性。顾名思义,常量池主要用于JVM运行时保存类执行文件中的各种常量,有些常量是在编译阶段优化生成而在运行时加载,有些常量是关于类的基本信息或者属性信息,有些常量在运行时作为JVM指令的操作数,后面章节将展开详细的论述。
2 类文件组织结构
Java语言源文件是程序代码的文本文件,经JVM编译,生成以字节码(二进制)的形式表示而非文本形式表示的类文件,文件中包含可执行的JVM指令以及类属性、类方法以及类相关的元数据信息,其中JVM指令是一种类似计算机的指令,包括指令操作码、指令操作数,JVM在运行时使用类加载器加载该字节码类文件,JVM运行的应用执行业务逻辑处理时调用该类文件中的方法。
由于JVM是使用C语言开发,字节码类文件是以C语言数据结构组织存储,其数据结构如下所示:
其中,u2、u4是基本的无符号存储单元,u2表示2个字节占用空间,u4表示4个字节占用空间,其他是类型是基本存储单元的连续存储单元,下表是对各字段的简要说明。
字段名称 | 字段类型 | 字段描述 |
magic | u4 | 这个字段使用固定的值:0xCAFEBABE,其表示的意义类文件的格式 |
minor_version | u2 | 类文件小版本号 |
major_version | u2 | 类文件大版本号 |
constant_pool_count | u2 | 常量池大小 |
constant_pool[constant_pool_count-1] | cp_info | 常量池数组 |
access_flags | u2 | 访问权限标识 |
this_class | u2 | 常量池的一个索引值,对应常量池的实体表示类的信息 |
super_class | u2 | 常量池的一个索引值,对应常量池的实体表示类的继承类 |
interfaces_count | u2 | 类实现的接口数量 |
interfaces[interfaces_count] | u2 | 类实现的接口数组 |
fields_count | u2 | 类域(类变量或者实例变量)的数量 |
fields[fields_count] | field_info | 类域数组 |
methods_count | u2 | 类或者接口方法的数量 |
methods[methods_count] | method_info | 类或者接口方法数组 |
attributes_count | u2 | 属性的数量 |
attributes[attributes_count] | attribute_info | 属性数组 |
2.1 常量池大小
constant_pool_count该值等于constant_pool数组的长度加1。
2.2 常量池数组
常量池也叫常量表,实际上是一个数组,数组存储的实体表示各种不同的字符串常量、类或者接口名称及其属性域、类结构及其类子结构中涉及到的常量。其中,每个实体的起始字节表示其常量的类型。常量池的索引值的范围大于或等于1,小于或等于constant_pool_count-1。
3 常量池实体数据结构
常量池实体的数据结构如下所示:
其中,字段tag标识实体的类型,字段info[]标识指定实体的附加信息,该附加信息根据实体类型的不同而不同。
下表列出常量池中的实体类型:
如上表所示,其中,Constant Kind列表示常量的字面描述类型,Tag列表示常量池实体数据结构中的值类型,class file format列表示支持对应类型的类文件初始版本,Java SE列表示支持对应类型的的JDK初始版本。
JVM指令是以索引的方式从常量池中读取操作数常量,用于压入栈参与运算的,下表列出可用于栈参与运算的常量池实体类型:
4 字符串类型常量
字符串类型常量的数据结构如下所示:
以上数据结构作为字节序列存储在常量池实体类型数据结构cp_info的字段info数组中。其中tag的值等于8,string_index对应常量池的索引值,该索引值对应的实体是字符串常量所表示的通用数据结构CONSTANT_Utf8_info,该通用字符串数据结构如下所示:
在以上数据结构中,tag的值等于1,length表示bytes[length]数组的长度,bytes是存储字符串常量的字节序列数组。
5 字符串运算
在JDK中,字符串是一个常量,是java.lang.String类的实例化对象,并且所有常量表达式都是指向同一个常量,如下代码样例展示字符串常量的对比分析。
由以上的分析可知:
-
同一个类包同一类下的相同字符串指向同一字符串对象
-
同一个类包不同类下的相同字符串指向同一个字符串对象
-
不同类包不同类下的相同字符串指向同一个字符串对象
-
常量表达式是在编译阶段实行计算,其指向同一个字符串对象
-
使用连接操作符号(+)连接字符串(非常量表达式),是在运行时阶段实行计算,其指向不同的字符串对象
-
使用字符串intern()方法显式地计算,其指向同一个字符串对象
其中,"Hel" + "lo"是常量表达式,而"Hel" + lo不是常量表达式。
6 常量池字节码
下图代码样例展示常量表达式的字节码,表示在编译阶段生成字符串常量t=”abc”,并且在常量池中的索引地址是#19,其中,JVM指令ldc表示从常量池中取操作数实行压栈操作。