Java 类文件结构

无关性的基石

实现语言无关性的基础仍然是虚拟机和字节码存储格式。Java 虚拟机不与包括 Java 语言在内的任何程序语言绑定,它只与“Class 文件”这种特定的二进制文件格式相关联。Class 文件中包含了 Java 虚拟机指令集、符号表以及若干其他辅助信息

Class 类文件的结构

  • 任何一个 Class 文件都对应着唯一的一个类或接口的定义信息。

  • Class 文件是一组以 8 个字节为基础单位的二进制流

  • Class 文件格式采用一种类似于 C 语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:

    “无符号数”和“表”。

    • 无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数。
    • 无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成的字符串值。
    • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型。
    • 无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式。
  • 顺序为“Big-Endian”,即高位字节在地址最低位,最低字节在地址最高位

类文件结构

魔数与 Class 文件的版本

常量池

  • 常量池的入口需要放置一项 u2 类型的数据,代表常量池容量计数值。
  • 容量计数是从 1 而不是 0 开始的。
    • 在特定情况下需要表达“不引用任何一个常量池项目”的含义,可以将索引值设置为 0 来表示。
  • 常量池主要存放两大类常量:
    1. 字面量(Literal):接近于 Java 语言层面的常量概念,如文本字符串、被声明为 final 的常量值。
    2. 符号引用:属于编译原理方面的概念,如被模块导出或者开放的包、类和接口的全限定名、字段和方法的名称与描述符、方法句柄与方法类型、动态调用点与动态常量等。
  • 常量表中分别有 17 种不同类型的常量。

常量池示例 常量类型示例

  • tag 是标志位,用于区分常量类型,name_index 是常量池的索引值,指向常量池中一个 CONSTANT_Utf8_info 类型常量,此常量代表这个类或接口的全限定名。
  • CONSTANT_Utf8_info 型常量的最大长度也是 Java 中方法、字段名的最大长度,即 u2 类型能表达的最大值 65535。

访问标志

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

访问标志示例

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

  • 类索引(this_class)和父类索引(super_class)都是 u2 类型的数据,接口索引集合(interfaces)是一组 u2 类型的数据集合。
  • Class 文件中由这三项数据来确定该类型的继承关系。
    • 类索引用于确定类的全限定名,父类索引用于确定父类的全限定名。
    • 实现的接口将按 implements 关键字(如果是接口,则按 extends 关键字)后的接口顺序从左到右排列在接口索引集合中。

字段表集合

  • 字段表(field_info)用于描述接口或类中声明的变量,包括类级变量及实例级变量,但不包括在方法内部声明的局部变量。
  • 字段可以包括的修饰符有字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final修饰符)、并发可见性(volatile修饰符)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。
    • 各个修饰符都是布尔值,要么有某个修饰符,要么没有,适合使用标志位表示。

字段修饰符示例

  • 字段修饰符放在 access_flags 项目中。

字段表示例

  • name_indexdescriptor_index 都是对常量池项的引用,分别代表字段的简单名称以及字段和方法的描述符。

字段表引用示例

  • 用描述符描述方法时,按照先参数列表、后返回值的顺序描述。对于数组类型,每一维度用一个前置的 [ 字符来描述,如一个定义为 java.lang.String[][] 类型的二维数组将被记录成 [[Ljava/lang/String;,一个整型数组 int[] 将被记录成 [I
  • 字段表可在属性表中附加描述零至多项的额外信息。
  • 字段表集合中不会列出从父类或父接口中继承的字段,但可能会出现原本 Java 代码中不存在的字段,如在内部类中为了保持对外部类的访问性,编译器会自动添加指向外部类实例的字段。

方法表集合

  • Class 文件存储格式中对方法的描述与字段的描述几乎一致。方法表的结构如同字段表一样,依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。

方法表集合示例

  • Java 代码经过 javac 编译器编译成字节码指令后,存放在方法属性表集合中一个名为 Code 的属性内。

Code 属性示例

  • 如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但可能会出现由编译器自动添加的方法,最常见的是类构造器 () 方法和实例构造器 () 方法。

特征签名

  • 一个方法中各个参数在常量池中的字段符号引用的集合。
  • 在 Java 语言中无法仅仅依靠返回值的不同来对一个已有方法进行重载。
  • 在 Class 文件格式中,如果两个方法有相同的名称和特征签名,但返回值不同,那么它们可以合法共存于同一个 Class 文件中。

属性表集合

  • Class 文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息
  • 允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息。
  • 对于每个属性,它的名称都要从常量池中引用一个 CONSTANT_Utf8_info 类型的常量来表示,而属性值的结构是完全自定义的,只需通过一个 u4 的长度属性说明属性值所占用的位数即可。

属性表集合示例

Code 属性

  • Java 程序方法体内的代码经过 javac 编译器处理后,最终变为字节码指令存储在 Code 属性内。

Code 属性详细示例

Exceptions 属性

  • Exceptions 属性列举出方法中可能抛出的受查异常(Checked Exceptions)。

LineNumberTable 属性

  • LineNumberTable 属性用于描述 Java 源码行号与字节码行号(字节码的偏移量)之间的对应关系。

LocalVariableTable 及 LocalVariableTypeTable 属性

  • LocalVariableTable 属性用于描述栈帧中局部变量表的变量与 Java 源码中定义的变量之间的关系。

SourceFile 及 SourceDebugExtension 属性

  • SourceFile 属性用于记录生成此 Class 文件的源码文件名称。

ConstantValue 属性

  • ConstantValue 属性的作用是通知虚拟机自动为静态变量赋值。只有被 static 关键字修饰的变量(类变量)才可以使用此属性。

InnerClasses 属性

  • InnerClasses 属性用于记录内部类与宿主类之间的关联。

Deprecated 及 Synthetic 属性

  • Deprecated 属性表示某个类、字段或方法已被程序作者定为不再推荐使用。
  • Synthetic 属性代表此字段或方法不是由 Java 源码直接产生的,而是由编译器自行添加的。

StackMapTable 属性

  • 此属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用。

Signature 属性

BootstrapMethods 属性

MethodParameters 属性

模块化相关属性

运行时注解相关属性

字节码指令简介

  • 字节码与数据类型
  • 加载和存储指令
  • 运算指令
  • 类型转换指令
  • 对象创建与访问指令
  • 操作数栈管理指令
  • 方法调用和返回指令
  • 异常处理指令
  • 同步指令

公有设计,私有实现