引子:
前面已经解析了一个class文件的魔数和class文件的版本号, 这篇文章重点要解析的就是class文件中的一个重要的部分——常量池.
class文件中的所有字面常量(包括类名, 方法名,字段名, 字段类型等等都是放在常量池中的)这样做的目的是因为class文件的其他部分可能都要使用到这些内容, 如果每个地方用到都保存一份那么太浪费空间, 所以把这些数据都放到常量池中, 其他地方要引用就直接引用常量池就可以了. 所以常量池是class文件中很重要的一部分.
1. 常量池结构
首先直观的看一个常量池到底长什么样(其实在class文件都是一堆的字节, 只不过每个字节在特定的上下文都代表指定的意思)
可以看到class文件的版本号后紧跟的就是常量池. 常量池的头两个字节表明了常量池中常量项的个数, 因为只有两个字节所以常量项是有数量限制的. 具体多少个可以自行计算. 常量项个数后面紧跟的就是各个常量项了. 每个常量项都有一个1个字节的tag标志位, 用于表示这个常量项具体代表的内容, 从图中可以看到如果tag是07的话就表示这是一个Class Info的常量项, 从名字就可以看出来这是一个表示Class信息的常量项.
用专业一点的术语描述的话常量池中保存的内容就是字面量和符号引用. 字面量就像类似于文本字符串, 声明为final的常量值. 符号引用包括3类常量类和接口的全限定名, 字段名称和描述符, 方法名称和描述符.
特别要注意的一点是常量池中的常量项的索引是从1开始的, 这样做的目的是满足后面其他结构中需要表明不引用任何一个常量项的含义, 这个时候就将索引值置为0.
2. 常量项的表示
从前面的描述可以总结出来, 所有的常量池项都具有如下通用格式:
cp_info {
u1 tag;
u1 info[];
}
常量池中,每个cp_info项(也就是常量项)的格式必须相同,它们都以一个表示cp_info类型的单字节"tag"项开头. 后面info[]项的内容由tag的类型所决定.
tag的类型有如下几种
3. 常用的常量项
-
Class Info
CONSTANT_Class_Info { u1 tag; u2 name_index; }
- tag的值为7
- name_index指向了常量池中索引为name_index的常量项
-
UTF8 Info
CONSTANT_UTF8_Info { u1 tag; u2 length; u1 bytes[length]; }
- tag的值为1
- length表示这个UTF8编码的字符串的字节数
- bytes[length]表示length长度的具体的字符串数据
注意: 因为class文件中的方法名, 字段名等都是要引用UTF8 Info的, 但是UTF8 Info的数据长度就是2个字节, 所以方法名, 字段名的长度最大就是65535.
-
String Info
CONSTANT_String_INFO { u1 tag; u2 string_index; }
- tag的值为8
- string_index指向了常量池中索引为string_index的常量项
-
Field_Ref Info
CONSTANT_Fieldref_Info { u1 tag; u2 class_index; u2 name_and_type_index; }
- tag的值为9
- class_index指向了常量池中索引为class_index的常量项, 且这个常量项必须为Class Info类型
- name_and_type_index指向了常量池中索引为name_and_type_index的常量项, 且这个常量项必须为Name And Type Info类型
-
Method_Ref Info
CONSTANT_Methodref_Info { u1 tag; u2 class_index; u2 name_and_type_index; }
- tag的值为10
- class_index指向了常量池中索引为class_index的常量项, 且这个常量项必须为Class Info类型
- name_and_type_index指向了常量池中索引为name_and_type_index的常量项, 且这个常量项必须为Name And Type Info类型
-
NameAndType Info
CONSTANT_NameAndType_Info { u1 tag; u2 name_index; u2 descriptor_index; }
- tag的值为12
- name_index指向了常量池中索引为name_index的常量项
- descriptor_index指向了常量池中索引为descriptor_index的常量项
以上这些就是常用的常量项, 或者说在我的mini jvm中常用的常量项, 至于其他的常量项也是类似, 可以参考oracle关于常量项的定义自行分析
3. 具体实现
因为我使用的java实现jvm, 所以以上的常量项其实就相当于是一个个的类, 解析的过程就是初始化一个一个的类. 具体的代码实现就不在这里讲述了, 请参考代码中的com.aaront.exercise.jvm.constant
这个包下的内容和ClassParser
这个类的实现
4. 访问标志、类和接口
这三个内容是紧跟在常量池之后的, 而且比较简单, 所以在这里简单介绍一下
- 访问标志
访问标志实际上就是当前class的修饰符, 看一下访问标志的定义就知道了
访问标志由2个字节表示, 这个class有哪个修饰符对应的位就置为1, 非常的简单
- 类
紧接在访问标志后面的是this class和super class, 这两个的值都是指向常量池的中某一个常量项同时所指向的常量项必须是Class Info类型
- 接口
紧接在类后面的是接口, 因为一个class可以实现多个接口, 所以和类不同的是接口需要有一个字段来标明这个类实现了多少个接口, 所以开头的两个字节是表明class实现的接口数量, class实现了几个接口后面就有几个接口数量, 这和class一样都是指向常量池中的某个常量项同时常量项的类型必须是Class Info类型.
4. 总结
常量的解析到此就结束了, 常量池是一个非常重要的结构, 整个class文件的各个部分包括后面要提到的字段, 方法, 属性中都有对常量池的引用, 也正是因为有了常量池class文件才可以节省一大部分的空间.
5. 代码地址
6. 本系列其他文章
手把手教你撸一个Mini JVM系列(1)之解析Class File -- 初探
手把手教你撸一个Mini JVM系列(3)之解析Class File -- 字段、方法、属性
手把手教你撸一个Mini JVM系列(4)之执行引擎
手把手教你撸一个Mini JVM系列(5)之源码分析 -- 常量池、访问标志、类索引
手把手教你撸一个Mini JVM系列(6)之控制流 -- 条件判断和循环