Class文件是一组以8位字节为基础单位的二进制流,各个数据项严格按照顺序紧凑地排列在Class文件中,中间没有任何分隔符,整个Class文件存储的内容几乎全是程序运行的必要数据,没有空隙存在。当遇到8位字节以上空间的数据项时,则按照高位在前的方式分割成若干个8位字节进行存储。
根据Java虚拟机规范,Class文件格式采用一种类似c语言结构体的伪结构来存储数据,这种伪结构中只有两种数据结构:无符号数和表,后面的解析都以这两种数据类型为基础。
- 无符号数:属于基本的数据类型,以u1、u2、u4、u8来分别代表一个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。
- 表:是由多个符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性的以 “ _info ” 结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质就是一张表。
类型 | 名称 | 数量 | 解释 |
---|---|---|---|
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 | 接口数量 |
u2 | 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 | 属性表 |
上面的表其实可以划分为以下七个部分,这七个部分组成了一个完整的 Class 字节码文件:
- 魔数与Class文件版本
每个Class的头4个字节成为魔数,唯一作用就是确定class文件时被虚拟机接受的(0xCAFEBABE),紧接着是版本号第5、6字节是次版本号,第7、8字节是主版本号,我们写一段程序编译成class来解释:
public class TestClass{
private int m;
public int inc(){
return m+1;
}
}
如图是用16进制编辑器查看,开头是0xCAFEBABE,次版本号0x0000,主版本号0x0034(即十进制52),
- 常量池
紧接着主版本号之后的是常量池入口,它是class文件中与其他项目关联最多的数据类型,也是class文件空间最大的数据项目之一,还是在class文件中第一个出现的表类型数据项目。
紧接着主版本号的u2类型 0x0013 表示常量池常量的个数(constant_pool_count),那么紧跟着就有 constant_pool_count - 1 个常量(从索引1开始,0号常量空出来代表不指向的时候考虑),使用javap -verbose TestClass.class
查看常量池信息:
拿第一个举例说明:对应的十六进制是0A 00 04 00 0F
第一个0A代表tag,代表着是一个方法表(CONSTANT_Methodref_info tag=10),00 04代表class_index(方法是哪个类的,4指向常量池的4号即Object),00 0F代表name_and_type_index,指向15号即方法名 方法签名类型()V。下面给出tag和表对应的关系:
- 访问标志
在常量池结束之后,紧接着的两个字节代表访问标记(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口、是否定义为public类型、是否定义为abstract类型等。具体的标志位以及标志的含义见下表
本例的字节码中这两个字节是 00 21,通过查看我们并没有发现有标志值是 00 21 的标志名称。这是因为这里的访问标志可能是由多个标志名称组成的,所以字节码文件中的标志值其实是多个值进行或运算的结果。
通过查阅上述表格,我们可以知道,00 21 由 00 01 和 00 20 进行或运算得来。也就是说该类的访问标志是 public 并且允许使用 invokespecial 字节码指令的新语义。 类索引、父类索引、接口索引
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。
类索引。类索引用于确定这个类的全限定名,它用一个 u2 类型的数据表示。这里的类索引是 00 03 表示其指向了常量池中第 3 个常量,通过我们之前的分析,我们知道第 3 个常量其最终的信息是 TestClass类。
父类索引。父类索引用于确定这个类的父类的全限定名,父类索引用一个u2类型的数据表示。这里的父类索引是 00 04 表示其指向了常量池中第 4 个常量,通过我们之前的分析,我们知道第 4 个常量其最终的信息是 Object 类。因为其并没有继承任何类,所以 TestClass类的父类就是默认的 Object 类。
接口索引。接口索引集合就用来描述哪个类实现了哪些接口,这些被实现的接口将按 implements 语句(如果这个类本身就是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。对于接口索引集合,入口第一项是 u2 类型的数据为接口计数器(interfaces_count),表示索引表的容量,而在接口计数器后则紧跟着所有的接口信息。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。
这里 TestClass类的字节码文件中,因为并没有实现任何接口,所以紧跟着父类索引后的两个字节是0x0000,这表示该类没有实现任何接口。因此后面的接口索引表为空。字段表集合
字段表集合用于描述接口或者类中声明的变量。这里说的字段包括类级变量和实例级变量,但不包括在方法内部声明的局部变量。
在类接口集合后的2个字节是一个字段计数器,表示总有有几个属性字段。在字段计数器后,才是具体的属性数据。字段表的每个字段用一个名为 field_info 的表来表示,field_info 表的数据结构如下所示:
本例中fileds_count对应0x0001又一个字段,紧接着后面是field_info00 02 00 05 00 06 00 00
access_flags对应0x0002查找对应表:
可知该字段是private修饰,name_index对应0x0005查找常量池字段名m,descript_index对应0x0006查找常量池索引6得到I,name这个I代表什么呢?字段类型?
可以看出对应的是int类型方法表集合
在字段表后的 2 个字节是一个方法计数器,表示类中总有有几个方法。在字段计数器后,才是具体的方法数据。方法表中的每个方法都用一个 method_info 表示,其数据结构如下:
methods_count对应:0x0002
代表有两个method_info,有两个方法我们就查看第二个方法inc()
这是一个查看字节码的可视化工具,在文章开头有链接,先看access_flag对应ox0001
,查看方法访问标识表:
可知该方法是public,name_inde对应0x000B
查找常量池可知方法名inc
descriptor_index对应0x000C
可知方法签名()I- 属性表集合
属性表在前面已经出现了很多次,类文件、方法表、字段表都可以携带自己的属性表集合
Code属性
Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性。
1.attribute_name_index:指向常量池的索引,固定为Code
2.max_stack:操作数栈深度的最大值
3.max_locals:局部变量表所需的存储空间。max_locals的单位为Slot,小于32位的值(如int、byte、float)占用1一个Slot,double、long占用两个Slot。并不是方法中定义了多少个局部变量,就把这些局部变量所占Slot之和作为max_locals的值,当代码执行超过一个局部变量的作用域时,这个局部变量占用的Slot就可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配Slot给各个变量使用,然后计算max_locals的大小。
4.code_length:字节码长度
5.code:用于存储字节码指令的一系列字节流。每个指令都是一个u1类型数据,当虚拟机读取到code的一个字节码时,就可以找出这个字节码代表的是什么指令,并且可以知道这条指令后面是否需要跟随参数,以及参数应当如何理解。
6.exception_table:异常表如下。如果当字节码在第start_pc行到第end_pc行之间(不含end_pc行)出现了类型为catch_type或者其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的引用),则转到第handler_pc行继续处理。当catch_type的值为0时,代表任意异常情况都需要转向到handler_pc行进行处理。
异常表实际上是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常及finally处理机制。
Exceptions属性
Exceptions属性:方法可能抛出的异常
LineNumberTable
Java源码行号与字节码行号之间的对应关系,当抛出异常时,行号就是从这里获取到的。
LocalVariableTable
描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。如果没有这个表,在调试期间无法获得参数值。