类文件结构(第六章)

135 阅读8分钟

Class类文件结构

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

  • 无符号数属于基本数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数据、索引引用、数值量或者按照UTF-8编码构成字符串值。
  • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型、为了便于区分,所有表的命名都习惯心的以“_info”结尾。

魔数与Class文件的版本

每个class文件的头4个字节被称为魔数,它唯一的作用就是标识这个文件是否能被虚拟机接受的class文件。魔数值为0xCAFEBABE,使用魔数作为文件的身份识别而不是拓展名,是基于安全考虑,因拓展名可以随意修改。

接着魔数后面的4个字节存储的是class文件的版本号:第5和第6个字节是次版本号,第7和第8个字节是主版本号。

常量池

紧接着主次版本号之后的是常量池入口,常量池可以比喻为class文件里的资源仓库,它是class文件结构中与其他项目关联最多的数据。它主要存放着两大类常量:字面量和符号引用

字面量: 字面量比较接近于Java语言层面的常量概念。

符号引用: 主要包括以下几类常量

  • 被模块引入或开放的包(package)
  • 类和接口的权限定名
  • 字段的名称和描述符
  • 方法的名称和描述符
  • 方法句柄和方法类型
  • 动态调用点和动态常量

class文件的每个常量都是一个表。在class文件中不会保存各个方法、字段在内存中的布局信息,这些字段、方法的符号引用最终在虚拟机运行时转换后才能真正得到内存入口地址。

访问标志

在常量池结束之后,紧接着的2个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否声明为final;等等;

类索引、父类索引与接口索引集合

类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合。class文件中用这三项数据类型来确定该类型的继承关系。类索引用来确定这个类的权限定名,父类索引用来确定这个类的父类的权限定名,接口索引集合就用来描述这个类实现了哪些接口。

字段表字段

字段表用于描述接口或者类声明的变量。Java语言中的字段包括类级变量以及实例变量,但不包括方法内部声明的局部变量。

方法表集合

class文件中对方法的描述与字段的描述采用了几乎完全一致的方式。方法表的结构如同字段表一样,依次包括访问标志、名称索引、描述符索引、属性表集合

属性表集合

属性表是class文件、字段表方法表等携带自己属性的集合,以描述某些场景专有的信息。

  1. Code属性:Java程序方法体里面的代码经过编译处理后,最终变为字节码指令存储在Code属性内。Code属性表的结构有max_stack(代表操作数栈深度的最大值)、max_locals(代表局部变量所需的存储空间,max_localsd的单位是变量槽)、code_length和code用来存储Java源程序编译后生成的字节码指令。
  2. Exceptions属性:这里的Exceptions属性的作用是列举出方法中可能抛出的受检查异常,也就是方法描述时在throws关键字后面列举的异常。
  3. LineNumberTable属性:用于描述Java源码行号与字节码行号之间的对应关系。
  4. ConstantValue属性:作用是通知虚拟机自动为静态变量赋值。目前虚拟机对类变量和实例变量的赋值有所不同,实例变量在方法中进行(即构造方法),对于类变量,如果同时使用static和final,则通过ConstantValue属性进行初始化,如果只用static,则在方法中进行初始化(即类初始化)。等等

字节码指令简介

Java虚拟机的指令是由一个字节长度的、代表着某种特定含义的数字(操作码)以及跟随其后的零至多个代表此操作所需的参数(操作数)构成。

字节码与数据类型

在Java虚拟机的指令集中,大多数指令都包含在其操作所对应的数据类型信息。例如,iload指令用于从局部变量表中加载int类型的数据到操作数栈中,fload指令加载到数据类型则是float。等等

加载与存储指令

加载和存储的指令用于将数据在栈帧中的局部变量表和操作数栈之前来回传输,这类指令包括:

  • 将一个局部变量加载的操作数栈,如iload、iload_、lload、fload等等
  • 将一个数值从操作数栈存储到局部变量表,如istore、fstore等等
  • 将一个常量加载到操作数栈,如bipush、sipush、ldc、iconst_m1等等

运算指令

算术指令用于对操作数栈上的两个值进行某种特定的运算,并把结果重新存入到操作数栈顶。常见指令如下:

  • 加法指令:iadd、ladd
  • 减法指令:isub、lsub
  • 位移指令:ishl、ishr
  • 按位与指令:ior、lor
  • 局部变量自增指令:iinc
  • 比较指令:dcmpg、dcmpl等等

类型转换指令

类型转换指令可以将两种不同的数值类型相互转换。

对象创建指令与访问指令

虽然实例和数组都是对象,但Java虚拟机对实例和数组的创建与操作使用了不同的字节码指令。对象创建后,就可以通过对象的访问指令获取对象的实例或者数组实例中的字段或者数组元素,这些指令包括:

  • 创建实例的指令:new
  • 创建数组的指令:newarray、anewarray、multianewaaray
  • 访问类字段和实例字段的指令:getfield、putfield、getstatic、putstatic
  • 把一个数据元素加载到操作数栈的指令:baload、caload、saload等
  • 将一个操作数栈的值存储到数组元素中的指令:bastotre、castore
  • 取数组长度的指令:arraylength

操作数栈管理指令

如同操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作数栈的指令,包括:

  • 将操作数栈的栈顶一个或者两个元素出栈:pop、pop2
  • 赋值栈顶一个或者两个数值并将复制值或者两份的复制值重新压入栈顶:dup、dup2
  • 将栈最顶端的两个数值互换:swap

控制转移指令

控制转移指令可以让Java虚拟机有条件或者无条件地从指定位置指令的下一条指令继续执行程序,从概念模型上理解,可以认为控制指令就是在有条件或者无条件地修改PC寄存器的值。控制指令包括:

  • 条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull等等
  • 复合条件分支:tableswitch、lookupswtich
  • 无条件分支:goto、ret等

方法调用指令和返回指令

常用指令:

  • invokevirtual指令:用于调用对象的实例方法
  • invokeinterface指令:用于调用接口方法,它会在运行时搜索实现了这个接口方法的对象,找出合适的方法进行调用
  • invokespecial指令:用于调用一些特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法
  • invokestatic指令:用于调用静态方法
  • invokedynamic指令:用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。

异常处理指令

在Java程序中显式抛出异常的操作都由athrow指令来实现。

同步指令

同步一段指令集序列通常由Java语言中的synchronized语句块来表示,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义。方法中调用过的每条monitorenter指令都必须有其对应的monitorexit指令,而无论这个方法上正常结束还是异常结束。