• 类和接口并不一定由类文件定义,也可以动态生成(可以参考 Java运行时动态生成class的方法 - 廖雪峰的官方网站 (liaoxuefeng.com)),直接送入类加载器中
  • Class 文件的是以 8 字节为单位的二进制流,当遇到需要使用 8 字节以上的空间的时候,将内容按照 8 字节切分,并且高位在前(大端序)
  • Class 文件使用类似于 C 语言的结构体储存数据,这种“结构体”中的字段只有两种类型:无符号数(u1, u2, u3, u4 代表占用不同的字节的无符号数)和表(使用多个无符号数和其他表,一个嵌套定义)

将整个 class 文件视作一个表,那么表的结构如下(下面的内容中将不会提到表中的字段的类型和占用空间,有需要请回到这里查阅)

魔数与版本

魔数是一个固定的值:0xCAFEBABE(显然是一个 16 进制数)

版本(主要是主版本号 major_version)用于判断类文件能否被一个 jvm 支持。

在使用 java 编译工具 javac 时,可以使用参数

  • -source:指定代码(.java)兼容的版本
  • -target:指定生成的(.class)文件的版本(即生成对应版本的 .class 文件)

现在副版本号(minor_version)只用于区分正式版(0x0000)和公测版(0xffff)

常量池

u2 类型的 costant_pool_count 代表常量池的大小(计数从 1 而非 0 开始,根据 u2 判断醉倒为 65536 项,注意是项而非字节)

常量池的索引为 0 值时代表的意义是不引用任何的常量池元素(类似于 null)(表类型中只有常量池的索引从 1 开始)

常量池中的元素分为字面量和符号引用

符号引用包括:

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

符号引用在类加载的连接阶段中被使用(见 Java 类加载 (kicey.site) ),被从一个常量转换为一个指针。

常量池中的每个常量都是一个表,以一个 u1 类型的标志位开始,表示常量的类型,对应关系如下

下面介绍一些主要的常量的表的结构(忽略 u1 的标志位),以 - 分隔大小和名称

CONSTANT_Utf8_info
  • u2 - length:代表长度
  • u1 * length - bytes:代表 utf-8 编码的自字符内容
CONSTANT_CLASS_info
  • u2 - name_index:CONSTANT_Utf8_info 类型值的索引(在常量池中的位置),表示一个类或接口的全限定名。
CONSTANT_NameAndType_info
  • u2 - index:指向该字段或方法的名称的常量池项的索引
  • u2 - index:指向该字段或方法的描述符的常量池项的索引
CONSTANT_MethodHandle_info
  • u1 - reference_kind:方法句柄的类型(和方法句柄的字节码行为相关)
  • u2 - reference_index:常量池索引
CONSTANT_MethodType_info
  • u2 - descriptor_index:表示方法的描述符,指向常量池中 CONSTANT_Utf8_info 的项
CONSTANT_Integer_info
  • u4 - bytes:int 值
CONSTANT_Float_info
  • u4 - bytes:float 值
CONSTANT_Long_info
  • u8 - bytes:long 值
CONSTANT_Double_info
  • u8 - bytes:double 值
CONSTANT_String_info
  • u2 - index:指向 CONSTATN_Utf8_info 的索引
CONSTANT_Fieldref_info
  • u2 - index:指向 CONSTATN_Class_info 的索引
  • u2 - index:指向 CONSTATN_NameAndType_info 的索引
CONSTANT_Methodref_info
  • u2 - index:指向 CONSTATN_Class_info 的索引
  • u2 - index:指向 CONSTATN_NameAndType_info 的索引
CONSTANT_InterfaceMethodref_info
  • u2 - index:指向 CONSTATN_Class_info 的索引
  • u2 - index:指向 CONSTATN_NameAndType_info 的索引
CONSTANT_Dynamic_info
  • u2 - bootstrap_method_attr_index:对引导方法表的 bootstrap_methods[] 数组的索引
  • u2 - name_and_type_index:对常量池中 CONSTANT_NameAndType_info 的索引
CONSTANT_InvokeDynamic_info
  • u2 - bootstrap_method_attr_index:对引导方法表的 bootstrap_methods[] 数组的索引
  • u2 - name_and_type_index:对常量池中 CONSTANT_NameAndType_info 的索引
CONSTANT_Module_info
  • u2 - name_index:CONSTANT_Utf8_info 的索引,表示模块的名字
CONSTANT_Package_info
  • u2 - name_index:CONSTANT_Utf8_info 的索引,表示包的名字

类/接口的访问标志

一个 u2 类型的值,表示类或接口的访问信息,具体如下(对标志值使用位运算与即可获得最终的访问标志)。

类索引

一个 u2 类型的值,表示一个常量池中 CONSTANT_Class_info 的项的索引

父类索引

一个 u2 类型的值,表示一个常量池中 CONSTANT_Class_info 的项的索引

这里就有之前提到的常量池索引从 1 开始的作用了,Object 类的父类索引为 0!

接口集合

一个 u2 的 interfaces_count 表示接口索引的数量

interfaces_count 个 u2 类型的值,表示一个常量池中 CONSTANT_Class_info 的项的索引

字段表集合

用于描述接口和类中声明的变量(字段),包括类字段和实例字段。fields_count 表示字段的数量,每一项的结构如下:

字段访问标志位

access_flags

最终同样是通过与运算得到

简单名称

name_index,指向常量池中的 CONSTANT_Utf8_info 项

相对于包括包名的全限定名,只有字段或方法的名字

字段描述符

descriptor_index,指向常量池中的 CONSTANT_Utf8_info 项

描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。

类型的表示如下:

对于数组类型,每一维度将使用一个前置的 "[" 字符来描述,如一个定义为 "java.lang.String[][]" 类型的二维数组将被记录成 "[[Ljava/lang/String;",一个整型数组 "int[]" 将被记录成 "[I" 。

用描述符来描述方法时,按照先参数列表、后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号 "()" 之内。如方法 void inc() 的描述符为 "()V",方法 java.lang.String toString() 的描述符 为 "()Ljava/lang/String; ",方法int indexOf(char[]source, int sourceOffset, int sourceCount, char[]target, int targetOffset, int targetCount, int fromIndex) 的描述符为 "([CII[CIII)I" 。

字段表集合中不会列出从父类或者父接口中继承而来的字段,但有可能出现原本Java代码之中不 存在的字段,譬如在内部类中为了保持对外部类的访问性,编译器就会自动添加指向外部类实例的字 段。另外,在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使 用不一样的名称,但是对于Class文件格式来讲,只要两个字段的描述符不是完全相同,那字段重名就 是合法的。

属性

attributes_count 和 attributes 都是对字段值的描述。

方法表集合

Class 文件存储格式中对方法的描述与对字段的描述采用了几乎完全一致的方式,方法表的结构如同字段表一样。

方法访问标志

属性表集合

Class 文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。 与 Class 文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松一 些,不再要求各个属性表具有严格顺序,并且《Java虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识 的属性。

属性类型

对于每一个属性,它的名称都要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示, 而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。结构如下:

属性表的比起其他内容稍显复杂,放在一篇新的博客中:Java 类文件属性表 (kicey.site)