Class 类文件的结构 (#6.3)

Class 文件是一组以 8 位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 Class 文件之中,中间没有添加任何分隔符,这使得整个 Class 文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用 8 位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个 8 位字节进行存储。

Class 文件格式采用一种类似于 C 语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数,后面的解析都要以这两种数据类型为基础。

无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值。

是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件本质上就是一张表,它由表 6-1 所示的数据项构成。

Class文件格式

名称 描述
魔数与 Class 文件的版本 头 4 个字节称为魔数(Magic Number),作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件;紧接着魔数的 4 个字节存储的是Class 文件的版本号:第 5 和第 6 个字节是次版本号(Minor Version),第 7 和第 8 个字节是**主版本号(Major Version)
常量池 可以理解为 Class 文件之中的资源仓库,它是 Class 文件结构中与其他项目关联最多的数据类型,也是占用 Class 文件空间最大的数据项目之一,同时它还是在 Class 文件中第一个出现的表类型数据项目。
常量池中主要存放两大类常量:字面量(Literal)符号引用(Symbolic References)。字面量比较接近于 Java 语言层面的常量概念,如文本字符串、声明为 final 的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量: 类和接口的全限定名(Fully Qualified Name) 字段的名称和描述符(Descriptor) 方法的名称和描述符
访问标志 紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final 等
类索引、父类索引与接口索引集合 类索引(this_class)父类索引(super_class)都是一个 u2 类型的数据,而接口索引集合(interfaces)是一组 u2 类型的数据的集合,Class 文件中由这三项数据来确定这个类的继承关系
字段表集合 字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量
方法表集合 Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样,依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。
方法的定义可以通过访问标志、名称索引、描述符索引表达清楚,但方法里面的代码去哪里了?方法里的 Java 代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里面。
属性表集合 在 Class 文件、字段表、方法表都可以携带自己的属性表(attribute_info)集合,以用于描述某些场景专有的信息。
在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个 Slot 位来存放对象实例的引用,方法参数值从 1 开始计算。这个处理只对实例方法有效。

字节码指令简介 (#6.4)

Java 虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。由于 Java 虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。

如果不考虑异常处理的话,那么 Java 虚拟机的解释器可以使用下面这个伪代码当做最基本的执行模型来理解,这个执行模型虽然很简单,但依然可以有效地工作:

1
2
3
4
5
6
do{
自动计算PC寄存器的值加1
根据PC寄存器的指示位置,从字节码流中取出操作码;
if(字节码存在操作数)从字节码流中取出操作数;
执行操作码所定义的操作;
}while(字节码流长度>0);
字节码与数据类型 (#6.4.1)

对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:i 代表对 int 类型的数据操作,l 代表 long,s 代表 short,b 代表 byte,c 代表 char,f 代表 float,d 代表 double,a 代表 reference。也有一些指令的助记符中没有明确地指明操作类型的字母,如 arraylength 指令,它没有代表数据类型的特殊字符,但操作数永远只能是一个数组类型的对象。还有另外一些指令,如无条件跳转指令 goto 则是与数据类型无关的。

Java 虚拟机的指令集对于特定的操作只提供了有限的类型相关指令去支持它,换句话说,指令集将会故意被设计成非完全独立的(Java 虚拟机规范中把这种特性称为“Not Orthogonal”,即并非每种数据类型和每一种操作都有对应的指令)。有一些单独的指令可以在必要的时候用来将一些不支持的类型转换为可被支持的类型。

表 6-31 列举了 Java 虚拟机所支持的与数据类型相关的字节码指令,通过使用数据类型列所代表的特殊字符替换 opcode 列的指令模板中的 T,就可以得到一个具体的字节码指令。如果在表中指令模板与数据类型两列共同确定的格为空,则说明虚拟机不支持对这种数据类型执行这项操作。例如,load 指令有操作 int 类型的 iload,但是没有操作 byte 类型的同类指令。

从表 6-31 中可以看出,大部分的指令都没有支持整数类型 byte、char 和 short,甚至没有任何指令支持 boolean 类型。因此,大多数对于 boolean、byte、short 和 char 类型数据的操作,实际上都是使用相应的 int 类型作为运算类型(Computational Type)

Java虚拟机指令集所支持的数据类型
Java虚拟机指令集所支持的数据类型

指令 描述
加载和存储指令 用于将数据在栈帧中的局部变量表和操作数栈(见第 2 章关于内存区域的介绍)之间来回传输
运算指令 用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶
类型转换指令 可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理本节开篇所提到的字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。
对象创建与访问指令 虽然类实例和数组都是对象,但 Java 虚拟机对类实例和数组的创建与操作使用了不同的字节码指令(在第 7 章会讲到数组和普通类的类型创建过程是不同的)。对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素
操作数栈管理指令 如同操作一个普通数据结构中的堆栈那样,Java 虚拟机提供了一些用于直接操作操作数栈的指令
控制转移指令 可以让 Java 虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序,从概念模型上理解,可以认为控制转移指令就是在有条件或无条件地修改 PC 寄存器的值
方法调用和返回指令 invokevirtual, invokeinterface, invokespecial, invokestatic, invokedynamic
异常处理指令 在 Java 程序中显式抛出异常的操作(throw 语句)都由 athrow 指令来实现,除了用 throw 语句显式抛出异常情况之外,Java 虚拟机规范还规定了许多运行时异常会在其他 Java 虚拟机指令检测到异常状况时自动抛出
同步指令 Java 虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的