概要:关于学习JVM类文件结构的笔记,主打一手精简但是带一点深度,这里是基于《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》一书进行学习的,但是其中有一些地方并不够清晰或者实例不够充分,这里也会讲述,当然只是拙见,有所不足敬请指出。
前提:类的结构
类文件的结构是以8位为基础单位的二进制流(原文说的是8字节,就之后的u1 u2都不是8个字节,肯定说不通……)。由两部分组成,无符号数与表。
无符号数:由1字节、2字节、4字节、8字节分别用u1、u2、u3、u8表示, 无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
表:由无符号数与表组成的复合数据结构。通常以_info结尾表示,class文件可以按哲学的方式也看做一张表……
class类文件是各部分按顺序紧密排列的,顺序如下表,这里附带了解释,以方便一样还在学习的朋友不会看着难受(interface info也与原文不一样,后面会说)
类型 | 名称 | 数量 | 解释 |
u4 | magic | 1 | 魔数 |
u2 | minor_version | 1 | 次版本号 |
u2 | major_version | 1 | 主版本号 |
u2 | constant_pool_count | 1 | 常量池数量 |
cp_info | constant_pool | constant_pool_count - 1 | 常量池信息 |
u2 | access_flags | 1 | 访问标志 |
u2 | this_class | 1 | 类索引 |
u2 | super_class | 1 | 父类索引 |
u2 | interface_count | 1 | 实现接口数量 |
interface_info | interfaces | interface_count | 接口索引表 |
u2 | fields_count | 1 | 字段数量 |
field_info | fields | fields_count | 字段信息 |
u2 | methods_count | 1 | 方法数量 |
method_info | methods | methods_count | 方法信息 |
u2 | attributes_count | 1 | 属性数量 |
attribute_info | attributes | attributes_count | 属性信息 |
1、魔数与版本号
魔数(magic bumber)用来确认一个文件是否为jvm可以接受的格式,class类文件的是0xCAFEBABE,如下图。版本号紧跟在魔数之后,占用4个字节,前两位是次版本号,后两位是主版本号,java版本号从45(jdk1)开始,如下图,0x33 = 51(D),算下来也就是jdk7(这里用jdk7是为了和书上一致)。
2. 常量池
根据最前面的表,过了魔数和版本号,就是常量池了。常量池算是最麻烦的一部分了。其有两部分,1是计数,2是内容。计数是一个u2(也就是两个字节),用来表示有多少项常量,然后是内容,对应一个一个的常量项。这里提供一个实例:
public class ModelFinal implements InterfaceB, InterfaceC {
private final static Long MODEL_LONG = 1L;
private final int modelInt = 99;
private String modelMsg;
public ModelFinal() {
}
public ModelFinal(String modelMsg) {
this.modelMsg = modelMsg;
}
public String modelInfo() {
return modelMsg + modelInt + MODEL_LONG;
}
}
其父接口都是空的,主要为了验证后面的interface集合。用jdk7编译后,使用winhex打开class文件,用数据显示器可以看到常量计数总数是60,但是使用的常量的计数索引是从1开始的,意思就是只有59个常量项,还有一个索引0是留着给没有引用常量的地方使用的(59+1=60)。
知道了常量池的构成后,就讲详细的常量项是怎么样的,任何一个常量项,它的第一个u1,都是表明它的类型的,比如上面那张图,接着的一个常量项就是0A,可以查表找到它的意义,如下
然后再去查表知道它的具体结构,如下图,具体的表太多了,这里贴篇博客参考:JVM——类文件结构_u4acc-CSDN博客
如果只看字节码,哪我们要对照实在有点麻烦,每次都需要去查表,于是可以使用java自带的javap -verbose class类文件的命令,查看类的常量池,前面的代码反汇编后如下图(部分):
3. 访问标志
访问标志是用来描述类的,像public,final,static等等(不需要多讲吧?),是一个u2。在全是16进制中的文件里面怎么找到什么地方开始才是访问标志呢?以winhex为例,看下图。因为class的各部分是紧密存放的,我们的代码中可没有感叹号,然后看其是一个u2,而且后面都没有asscii码能表示的字符了,于是猜测这个是访问标志开始了。
光猜不行,我们得算一算。比如前面代码中的类
类标志是public,查下面这张访问标志表可知,acc_public和acc_super为真,直接加起来,
0x1+0x20=0x21,和上面的对的上。至于为什么要把他们加起来,这个本质上其实是用二进制存储的信息,不同位代表不同的状态,加起来说明有某两种状态。比如0x0001,0x0010,只以后两个16进制位举例,是00000001与00010000。也就是说,第1位置1表示public,第5位置1表示final,两位同时置1表示两者都有,都置1和把他们加起来也就一样了,计算机专业应该都能看懂。
4. 类索引、父类索引与接口索引集合
紧接着访问标志的的就是类和父类索引这些了,主要是描述这个类的继承关系。我们都知道java只能有一个父类(而且默认都是Object),所以类索引和父类索引肯定都只有一个,这二者都用u2表示。在winhex里面查访问标志后面的两位,可知类索引是0x0c也就是十进制12,父类索引是0x0d,也就是十进制13
查反汇编的表,如下图,索引项能对上。需要说的是,后面讲的某某索引,都是指其在常量池中的索引。
然后就是接口索引集合了,首先其一个u2表示有多少个集合,上面代码的例子中有两个接口,如下图
从字节码来看,21是标志位,然后4个字节的类与父类索引,然后就是接口集合了,这里确实是2,也就是两个。紧跟其后就是其索引,为0x0E和0x0F,也就是14与15。
反汇编查常量表,发现确实能对上。
此处需要勘误《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》,书上是这样写的,接口计数用的是u2,然后接口的索引列表还是用的u2,这明显是不对的,应该是一个表,表的每一项都是u2大小的索引,指向常量表中的项。
5. 字段表集合
首先把字节码贴上,方便后面参照
0x03开始是字段表集合,这里表示类一共有3个字段。作为验证,确实如此,见下图
字段的表结构如下
于是可知上面的0x03计数值后面跟着的前6个字节是一个字段的访问信息,简单名索引,描述符索引。然后是属性集合。
这里先看访问标志,0x001A。访问标志如下表
按private+final+static的结果是0x0002+0x0010+0x0008=0x001A,所以这是代码中第一个变量的访问标志。
然后是简单名称索引,这里是0x10,也就是常量池索引16的项(见下方),可见确实是对应的变量名。
然后是描述符索引,0x11,也就是常量池索引17:
也对的上。
然后是属性表集合,用于存放额外信息,这里因为是Long对象,所以没有,前面代码中的第二个变量是int,且有赋值,所以它是有这项属性的,快速对照:
第一个是权限描述符,private+final = 0x10+0x02=0x12
第二个是简单名称索引,0x12=18d,
第三个是描述符索引,0x13=19d,
,也就是int
第四个是属性集合大小,这里是1,所以有一项属性
第五个就是唯一的一项属性所以,0x14=20d,查索引20的常量
一切都对得上,已经完美了……属性表集合后面的属性表集合详细讲解
*全限定名与简单名:上面提到的类索引指向的就是全限定名。简单名就是字段和方法没有任何类型和参数等修饰的名字,如上面的MODEL_LONG。
*描述符: 用来描述字段 的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本类型和void都用一个大写字母来表示, 对象类型则用字符L加对象的全限定名来表示。
于是这里就可以知道上面的Ljava/lang/Long的意思了说明这是一个对象,权限的名是java/lang/Long。对于数组来说,会在其前面加一个[来描述,比如int[]就是[I,long[][]就是[[J。
方法的描述符放在方法集合里面讲。
===============================================
下班了,后面的以后再补上