字节码文件结构是一组以 8 位字节为基础的二进制流,各数据项目严格按照顺序紧凑地排列在 Class 文件之中,中间没有任何分隔符。在字节码结构中,有两种最基本的数据类型来表示字节码文件格式: 无符号数和表

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

表是由多个无符号数或者其他表作为数据项构成的复合数据类型,用于描述有层次关系的复合结构的数据,所有表都习惯性地以 _info 结尾。而 Class 文件的本质就是一张表,由下面的项组成:

无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的“集合”

魔数与文件版本

Class 文件的前 4 个字节被称为魔数(Magic Number),其值为 CAFEBABE, 紧接着的第 5 和 6 字节表示次版本号,7 和 8 字节表示主版本号。虚拟机必须拒绝执行超过其版本号的 Class 文件

Java 的版本号是从 45 开始的,JDK 1.1 之后 的每个 JDK 大版本发布主版本号向上加 1(JDK 1.01.1 使用了 45.045.3 的版本号),高版本的 JDK 能 向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件

关于次版本号,曾经在现代 Java 2 出现前被短暂使用过,JDK 1.0.2 支持的版本 45.045.3,JDK 1.1 支持版本 45.045.65535,从 JDK 1.2 以后,直到 JDK 12 之前次版本号均未使用,全部固定为零。而到了 JDK 12 时期,由于 JDK 提供的功能集已经非常庞大,有一些复杂的新特性需要以“公测”的形式放出,所以设计者重新启用了副版本号,将它用于标识“技术预览版”功能特性的支持。如果 Class 文件中使用了该版本 JDK 尚未列入正式特性清单中的预览功能,则必须把次版本号标识为 65535,以便 Java 虚拟机在加载类文件时能够区分出来

常量池

紧跟版本信息之后的是常量池信息,其中前两个字节(u2 类型)表示常量池容量计数值,这个计数值是从 1 开始的,这样设计的原因是——如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量”的含义,可以把索引值设置为 0 来表示

Class 文件结构中只有常量池的容量计数是从 1 开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从 0 开始

其后的不定长数据则表示具体的常量项,主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References), 字面量接近语言层面的概念,比如字符串、final 常量等;符号引用则属于编译原理的概念,主要包括:

  • 被模块导出或者开放的包 (Package)
  • 类和接口的全限定名 (Fully Qualified Name)
  • 字段的名称和描述符 (Descriptor)
  • 方法的名称和描述符
  • 方法句柄和方法类型 (Method Handle、Method Type、Invoke Dynamic)
  • 动态调用点和动态常量 (Dynamically-Computed Call Site、Dynamically-Computed Constant)

常量池的常量都是由 cp_info 表结构组成的,而且表结构不同其大小也不同。在 Java 虚拟机规范中一共有 14 种 cp_info 类型的表结构:

关于常量类型的个数,上述描述和《》有出入,是所描述的 JDK 版本不同所致,要以最新版本为准

每一种 CONSTANNT_xxx_info 表的具体结构如下:

可以看到第一个字节都是描述表类型的标志值,后面的字段意义各不相同

访问标志

在常量池结束之后,紧接着的两个字节代表类或接口的访问标记(access_flags), 具体含义如下表所示:

如果有多个访问标志,会以上述的标志值进行或运算得到,比如 0021 表示同时设置了 ACC_PUBLIC 和 ACC_SUPER (0001 or 0020)

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

类索引和父类索引都是一个 u2 类型的数据,而接口索引集合是一组 u2 类型的数据的集合,java 中除了 java.lang.Object 类以外,每个类的父类索引都不为 0

对于接口索引集合,第一项 u2 类型的数据为接口计数器(interfaces _count ),表示索引表的容量。如果该类没有实现任何接口,则该计数器值为 0

字段表集合

字段表集合用于描述接口或者类中声明的变量,包括类变量( static )和成员变量,但不包括方法中声明的局部变量。在接口索引集合后的两个字节是字段计数器,描述字段的个数,然后才是字段信息

每一个字段的信息用一个 field_info 表来表示,结构如下:

方法表集合

在字段表集合后面的是方法计数器,描述方法的个数,然后使用 method_info 表结构描述每个方法的信息

属性表集合

注意字段表和属性表的区别,字段表中的属性指 Java 源码中声明的类属性或者说变量,而属性表中的属性是用来描述 class 的一些信息,比如 SourceFile 属性记录了 class 的源文件名称,SourceDebugExtension 属性用于存储额外的代码调试信息等等