Java Class文件结构:常量池
1 概念的引入
Java的Class文件在描述类、属性、方法等时要用到一系列的“常量”。比如下面的属性:
int var;
描述这个属性,需要两个常量:
属性名称“var”是一个字符串常量;
属性的数据类型是int,在class文件中通过单个大写字母I(这也是一个字符串常量)表示int;
因此,上述的属性就可以通过两个字符串常量来描述(不包括冒号):
var:I
通过常量来描述类、属性、方法等的好处大概有三个
1
who knows?
:
复用常量,压缩class文件的尺寸。比如描述int m;这个属性时,就可以复用I(表示int)这个字符串了。
可使用精简的符号表示复杂的含义,比如上述的I表示int,进一步压缩class文件的尺寸。
方便扩展。当需要定义新的常量时,只需要在常量池中添加即可。
2 一些术语
2.1 全限定名(Fully Qualified Name)
全限定名=包名+类/接口的名称。显然,只有全限定名才能唯一定位类或者接口的位置,因此在class文件中一律使用全限定名。
但是,由于当初设计class文件时考虑不周,class文件内部使用的全限定名被定义为使用/分割,比如java/lang/Object,这和大家习惯的包名表达方式(java.lang.Object)不一致。不得已,JVM规范将class内部使用的全限定名称为“internal form”(内部形式)的全限定名。由于本文仅涉及class的内部结构,因此引用到全限定名时皆指诸如java/lang/Object的内部形式。
2.2 描述符(Descriptor)
在表达属性、方法时,除了其名称(全限定名)外,其他的特性刻画需要依靠描述符(Descriptor)。
2.2.1 属性描述符
属性描述符仅表达了属性的数据类型。1列出了属性描述符中数据类型的表示方式。大部分的数据类型描述方式一目了然,和数据类型的首字符是一一对应的。下面重点阐述对象、数组的描述符。
对象的描述符要特别注意两点:一是类名使用全限定名,二是最后的分号(;)
2
为什么对象的描述符最后要有一个这样的小尾巴呢?因为在属性描述符中,其他数据类型都是单个字母,很容易确定整个属性描述符的界限。但是在对象描述符中,类的全限定名长度是可变的,无法事先确定,因此只好使用分号(;)来表示对象的描述符的结束。其实在这里,能够表示对象的描述符结束的符号很多,只要不是常规字母和/即可,至于为什么选择了分号作为结束符,尚未见考证材料。
。比如属性定义:String str的描述符为:Ljava/lang/String;。
多维数组的描述符使用了多个[来表达,比如double[][]的描述符为[[D。
我们在方法描述符中会大量使用到属性描述符来描述参数列表。
描述符
数据类型
说明
B
byte
字节
C
char
字符
D
double
双精度
F
float
单精度
I
int
整数
J
long
长整数
L ClassName ;
reference
ClassName对象
S
short
短整数
Z
boolean
true/false
[
reference
一维数组
表 1: 属性描述符的数据类型定义
2.2.2 方法描述符
方法的描述符是指描述方法的返回值和参数列表的字符串,其中参数列表在前,返回值紧跟其后。其中,参数列表为空只给出()即可。返回值的类型除了常规的数据类型如1之外,如果为void使用V表示,这一点比较特殊,需要注意一下。2给出了几个示例。
方法定义
方法描述符
String doSomething(int a, double b,Thread t){...}
(IDLjava/lang/Thread;)Ljava/lang/String
void doSomething(int a,double b,Thread t){...}
(IDLjava/lang/Thread;)V
void doSomething(int[][] a)
([[I)V
表 2: 方法描述符示例
这里引申出一个比较有意思的问题:Java的方法允许有多少个参数[1, p92][2]?结论是,Java的方法最多允许255个单位的参数,其中int等类型的参数算作一个单位,long和double类型的参数算作2个单位
3
为什么long和double算作两个2单位?这大概和JVM设计之初的一个不合理规定有关,参见xxx。
。这个问题作者还没有搞的太清楚,应该需要从Java虚拟机调用方法的栈机制去调查,从方法描述符的限制中找不到线索。
3 常量池的总体结构
常量池的总体结构如3所示,一个u2表示常量池的表项数量,后面紧跟着constant_pool_length个表项。这里要注意两点:第一,constant_pool_length不是常量池的总字节尺寸,而是表项数量。表项结构各不相同,因此常量池的总长度由所有表项的长度相加计算而得;第二,constant_pool_length的索引是从1开始的,因此真正的表项数量是constant_pool_length – 1。如1所示,我们的示例代码Person.class中,常量池表项的数量应为:0x0014 – 1项,即19项。
图 1: 常量池表项数量
数据类型
意义
数量
u2
constant_pool_length
1
u1
info[]
constant_pool_length – 1
表 3: 常量池的总体结构
到目前为止(Java SE 11),常量池共有17种常量表项,见4,每一种常量表项都有特定的数据结构。常量表项数据结构的第一个字节用于标识不同的表项,称为“标志”(Tag),比如Utf8编码的字符串表项(CONSTANT_Utf8_info)的标志为1,参见5。
常量表项类型
标志
Java SE
描述
CONSTANT_Utf8_info
1
1.0.2
Utf8编码的字符串
CONSTANT_Integer_info
3
1.0.2
整数
CONSTATN_Float_info
4
1.0.2
单精度浮点数
CONSTANT_Long_info
5
1.0.2
长整数
CONSTANT_Double_info
6
1.0.2
双精度浮点数
CONSTANT_Class_info
7
1.0.2
类
CONSTANT_String_info
8
1.0.2
字符串
CONSTANT_Fieldref_info
9
1.0.2
属性
CONSTANT_Methodref_info
10
1.0.2
方法
CONSTANT_InterfaceMethodref_info
11
1.0.2
接口方法
CONSTANT_NameAndType_info
12
1.0.2
名称和类型
CONSTANT_MethodHandler_info
15
7
方法句柄?
CONSTANT_MethodType_info
16
7
方法类型
CONSTANT_Dynamic_info
17
11
CONSTANT_InvokeDynamic_info
18
7
CONSTANT_Module_info
19
9
模块
CONSTANT_Package_info
20
9
包
表 4: 常量池的表项类型
常量池实际上是class文件的“符号体系”,所有在表达class文件的结构、意义和Java语法时会用到的“词汇”,均需要在常量池中定义,因此常量池的分量很重,往往是一个class文件的主要内容。常量池的这种设计也非常巧妙,通过扩展表项即可扩展Java语言的表达能力。
4 Utf8字符串常量
字符串常量是常量池中使用量最大的常量,其他的常量表项也经常引用字符串常量,比如CONSTANT_NameAndType_info,CONSTANT_Class_info等都会引用到字符串常量。众所周知,Java中的字符串是Utf8编码的,而Utf8是可变长度字符编码,使用一到六个字节为每个字符编码
4
参见:https://en.wikipedia.org/wiki/UTF-8
。因此Utf8编码的字符串表项(CONSTANT_Utf8_info)结构如5所示,需要使用一个u2类型的数据说明字符串的实际长度。
数据类型
描述
备注
u1
tag
标签=1
u2
length
字符串的实际长度
u1
bytes[length]
字符串
表 5: Utf8编码的字符串表项结构
Person.class中的其中一个(第一个)Utf8字符串常量如2所示。实际上,根据ghex这个软件右栏的提示,我们很容易找到其他的Utf8编码的字符串常量:本例中的Utf8编码常量均是一个字节的ASCII码字符,因此即很方便的显示在了ghex的右栏,也很容易通过数字节的方式识别出来。
图 2: Person.class的Utf8字符串常量示例
实际上,通过java -p更容易识别常量池项目。4是java -p的输出结果中关于常量池的部分,可以一目了然的看到,在常量池中共有19个表项,其中13个是Utf8编码的字符串常量,这些常量也很容易从hgex的右栏找出,如3所示。
图 3: 所有的Utf8编码的字符串常量
Constant pool:
#1 = Methodref #4.#16 // java/lang/Object."":()V
#2 = Fieldref #3.#17 // Person.age:I
#3 = Class #18 // Person
#4 = Class #19 // java/lang/Object
#5 = Utf8 age
#6 = Utf8 I|\longremark{属性的数据类型:int}|
#7 = Utf8 |\longremark{构造方法}|
#8 = Utf8 ()V|\longremark{方法的descriptor,这里是一个参数列表为空,返回值为void的方法描述符}|
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 isAdult
#12 = Utf8 ()Z|\longremark{方法的描述符(descriptor),参数列表为空,返回值为boolean}|
#13 = Utf8 StackMapTable
#14 = Utf8 SourceFile
#15 = Utf8 Person.java
#16 = NameAndType #7:#8 // "":()V|\longremark{由\#7和\#8号Utf8字符串组成的名称和类型常量表项}|
#17 = NameAndType #5:#6 // age:I|\longremark{由\#5和\#6号Utf8字符串常量表项组成的名称和类型常量表项}|
#18 = Utf8 Person
#19 = Utf8 java/lang/Object
|\showremarks|
5 整数常量(Integer)和单精度浮点数(Float)常量
CONSTANT_Integer_info和CONSTANT_Float_info的结构类似,如6所示。
数据类型
描述
备注
u1
tag
标签=3
u4
bytes
整数
表 6: 整数常量的表项结构
数据类型
描述
备注
u1
tag
标签=4
u4
bytes
单精度浮点数
表 7: 单精度浮点数常量的表项结构
Person.class中没有整数常量。
6 长整数(Long)和双精度浮点数(Double)常量
长整数(Long)和双精度浮点数(Double)都是8个字节存储的,因此其在常量池中的数据结构分别如8和9所示。
数据类型
描述
备注
u1
tag
标签=5
u4
high_bytes
u4
low_bytes
表 8: 长整数常量的表项结构
数据类型
描述
备注
u1
tag
标签=6
u4
high_bytes
u4
low_bytes
表 9: 双精度浮点数常量的表项结构
Person.class中没有长整数常量和双精度浮点数常量。
7 类(Class)常量
整个class文件不就表示了一个类吗?这里的类常量只是表达了类的名称而已,其结构如10所示。类名的索引指向了一个Utf8编码的字符串常量,因此Person.class的类常量如4所示,其中Person是类本身,java/lang/Object是Person的父类。我们知道,如果没有特别声明父类,则每一个类(Object本身除外)都会自动从Object继承下来,所以在常量池中一定会存在Object这个类常量。
图 4: Person.class中的类常量示例
数据类型
描述
备注
u1
tag
标签=7
u2
name_index
类名的索引
表 10: 类常量的表项结构
接口也是通过类常量来表示的。在《从C到Java》一书中,作者曾有阐述,接口就是纯的抽象类,即接口其实是类的一种。类常量可以用来表达接口更加确认了这一点。比如7所示的简单的接口定义,其javap -v的输出如2所示,其常量池有两个类常量,一个是IPerson,一个是java/lang/Object,即IPerson接口是从Object继承下来的:IPerson是一个经过包装的抽象类。
public interface IPerson{
boolean isAdult();
}
Classfile /home/subaochen/git/blog/src/java/IPerson.class
Last modified 2018年12月9日; size 119 bytes
MD5 checksum 2b865ff3610a20333b6df52384c1c38d
Compiled from "IPerson.java"
public interface IPerson
minor version: 0
major version: 55
flags: (0x0601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
this_class: #1 // IPerson
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 1, attributes: 1
Constant pool:
#1 = Class #7 // IPerson
#2 = Class #8 // java/lang/Object
#3 = Utf8 isAdult
#4 = Utf8 ()Z
#5 = Utf8 SourceFile
#6 = Utf8 IPerson.java
#7 = Utf8 IPerson
#8 = Utf8 java/lang/Object
{
public abstract boolean isAdult();
descriptor: ()Z
flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT
}
SourceFile: "IPerson.java"
8 字符串(String)常量
字符串常量用来表示一个String的实例,其指向了一个Utf8编码的字符串常量,其数据结构如11所示。
数据类型
描述
备注
u1
tag
标签=8
u2
string_index
指向Utf8编码的字符串常量
表 11: 字符串常量结构
注意区分字符串常量表项(CONSTANT_String_info)和Utf8编码的字符串常量表项(CONSTANT_Utf8_info)的不同用途:Utf8编码的字符串常量表项用来表示最终的字符串,也就是说,所有的字符串最终都是存储在Utf8编码的字符串常量表项中的;而字符串常量表项,仅用来表示String的实例所代表的字符串的索引,其真正的内容保存在Utf8编码的字符串常量表项中。
如果类中没有使用到String的实例,就不存在字符串常量表项,比如Person.java类。但是,每一个类中一定存在若干个Utf8编码的字符串常量表项用来表达类名、字段名等等。
9 名称和类型(NameAndType)常量
字段和方法通过描述符表达了字段和方法的类型,再加上名称(Name),就构成了CONSTANT_NameAndType_info,如12所示,其中name_index和descripter_index分别指向Utf8编码的字符串常量。
数据类型
描述
备注
u1
tag
标签=12(0x0C)
u2
name_index
字段和属性的名称索引
u2
descriptor_index
字段和属性的描述符索引
表 12: 名称和类型常量结构
Person.class中的名称和类型常量表项如5所示,第一个名称和类型常量表项由#7()和#8(()V)Utf8字符串常量表项组成,表达了构造方法的名称和类型;第二个名称和类型cl表项由#5(age)和#6(I)Utf8字符串常量表项组成,表达了字段age的名称和类型。
图 5: 名称和类型常量表项示例
从javap -v Person.class的输出中,更容易找到这两个名称和类型常量表项,如4所示。
10 字段、方法和接口方法常量
字段、方法以及接口的方法常量表项非常类似,如13、14、15所示。
数据类型
描述
备注
u1
tag
标签=9
u2
class_index
类名称索引
u2
name_and_type_index
名称和类型索引
表 13: 字段常量结构
数据类型
描述
备注
u1
tag
标签=10(0x0A)
u2
class_index
类名称索引
u2
name_and_type_index
名称和类型索引
表 14: 方法常量结构
数据类型
描述
备注
u1
tag
标签=11(0x0B)
u2
class_index
类名称索引
u2
name_and_type_index
名称和类型索引
表 15: 接口方法常量结构
Person.class的字段常量表项如6所示,方法常量表项如7所示。结合javap -v person.class的输出更容易解读这两个常量表项,此处截取片段说明:
#1 = Methodref #4.#16 // java/lang/Object."":()V
#2 = Fieldref #3.#17 // Person.age:I
#3 = Class #18 // Person
#4 = Class #19 // java/lang/Object
...
#16 = NameAndType #7:#8 // "":()V
#17 = NameAndType #5:#6 // age:I
Person.class中没有接口方法常量表项。
图 6: Person.class的字段常量表项示例
图 7: Person.class中的方法常量表项示例
由于类的方法和接口的方法使用了不同的常量表项来描述,因此方法常量表项中的class_index必须是类,不能是接口;同样的,接口方法常量表项中的class_index必须是接口,不能是类。
字段常量表项中的class_index即可以是类,也可以是接口。
11 方法句柄(MethodHandle_info)常量表项
数据类型
描述
备注
u1
tag
标签=15(0x0F)
u21
reference_kind
u2
reference_index
表 16: 方法句柄常量结构
12 方法类型(MethodType_info)常量表项
这个表项很简单,代表了方法的类型,即方法的描述符,其结构如17所示。但是这个常量和NameAndType_info不是重复了吗?MethodType_info在什么地方会用到呢?有待进一步考察。
数据类型
描述
备注
u1
tag
标签=16(0x10)
u2
descriptor_index
方法描述符索引
表 17: 方法类型常量结构
13 模块(Module_info)常量表项
模块常量用来表示一个模块,其结构如所示,其中name_index指向一个存储了模块名称的Utf8编码的字符串。
数据类型
描述
备注
u1
tag
标签=19(0x13)
u2
name_index
模块名称索引
表 18: 模块常量结构
14 包(Package_info)常量表项
包常量表示一个模块中输出或者打开的包,其结构如所示,其中name_index指向一个存储了包名称的Utf8编码的字符串。
数据类型
描述
备注
u1
tag
标签=20(0x14)
u2
name_index
包名称索引
表 19: 包常量结构
15 CONSTANT_Dynamic_info和CONSTANT_InvokeDynamic_info常量表项
TBD
引用
1Oracle, “JVM specification“.
2汪先生, “你知道Java方法能定义多少个参数吗?” (2018).
0