跳至主要內容

类文件结构

会敲代码的程序猿原创JVMJVM大约 19 分钟

类文件结构

计算机只能运行由0和1构成的二进制格式。 要运行Java程序,必须先通过Java虚拟机(JVM)执行编译后的Java代码,这个编译后的代码就是Java字节码,存储在.class类文件中。

跨平台的基石

Java字节码具有“平台无关性”和“语言无关性”。

  • 平台无关性: 字节码可以在任何支持JVM的平台上运行,实现“一次编写,到处运行”
  • 语言无关性: 多种编程语言可以编译成字节码并在JVM(GraalVM)上运行,不仅限于Java
Java虚拟机提供的语言无关性
Java虚拟机提供的语言无关性

Class类文件结构(理论)

Java技术的良好向后兼容性得益于Class文件结构的稳定性, 每个Class文件对应一个类或接口的定义信息,是一组以8个字节为单位的二进制流。各数据项严格按顺序排列,没有任何分隔符。

Class文件格式类似于C语言的结构体,这种伪结构只有两种数据类型:“无符号数”和“表”。

  • 无符号数: 基本数据类型,使用u1u2u4u8表示1、2、4、8个字节的无符号数。 它们可以描述数字、索引引用、数量值或按照UTF-8编码的字符串值。
  • 表: 由多个无符号数或其他表构成的复合数据类型,通常以“_info”结尾。 表用于描述有层次关系的复合结构,整个Class文件本质上也是一个表,由按严格顺序排列的数据项构成。

JVM虚拟机规范第四章open in new window 中规定了Class文件必须是一个固定的ClassFile结构,如下所示:

ClassFile {
  u4 magic;                   // 魔数 (0xCAFEBABE),标识class文件格式
  u2 minor_version;           // 次版本号
  u2 major_version;           // 主版本号
  u2 constant_pool_count;     // 常量池计数
  cp_info constant_pool[constant_pool_count-1]; // 常量池
  u2 access_flags;            // 访问标志
  u2 this_class;              // 当前类索引
  u2 super_class;             // 父类索引
  u2 interfaces_count;        // 接口计数
  u2 interfaces[interfaces_count]; // 接口索引表
  u2 fields_count;            // 字段计数
  field_info fields[fields_count];    // 字段表
  u2 methods_count;           // 方法计数
  method_info methods[methods_count];  // 方法表
  u2 attributes_count;        // 属性计数
  attribute_info attributes[attributes_count]; // 属性表
}

通过分析 ClassFile 的内容,我们便可以知道 class 文件的组成。

ClassFile 内容分析
ClassFile 内容分析

魔数

魔数: 用于验证文件是否为有效的Class文件,其值固定为0xCAFEBABE

  • 不仅限于Class文件,其他文件格式如GIFJPEG等,也使用魔数来进行身份识别。
    • GIF文件:47 49 46 38
    • JPEG文件:FF D8 FF E0

在Java被称为“Oak”语言时期(大约1991年前后),0xCAFEBABE被选为魔数。 Java开发小组关键成员Patrick Naughton提到,他们选择这个值是因为它好玩且容易记忆, 象征着著名咖啡品牌Peet’s Coffee深受欢迎的Baristas咖啡,也预示着日后“Java”这一商标名称的出现。

Class文件版本号

版本号: 紧跟魔数之后,占4个字节,包含主版本号和次版本号,用于标识Class文件的版本。

  • 第5~6字节:次版本号(Minor Version)
  • 第7~8字节:主版本号(Major Version),Java 8 = 52.0

Java 的主版本号从 JDK 1.0 的 45 开始,每次大版本发布都会+1; 次版本号通常保持为0,对应一些次要的特性改进或修复。

Class文件版本号:

支持高版本JDK编译出兼容低版本JDK的类,例如使用JDK 1.8版本,编译出1.7版本的class:

javac –source 1.8 –target 1.7 Example.java

注:从JDK 9开始,javac编译器不再支持使用-source参数编译版本号小于1.5的源码。

常量池

常量池: 紧随版本号之后,可以理解成Class文件的资源仓库。存放各种常量信息,如字符串常量、类和接口名、字段名和方法名等。

  • 与其他数据关联最多,占用空间最大,也是第一个出现的表类型数据项cp_info

1、常量池计数

由于常量项不固定,入口处u2类型的数据值表示常量池计数constant_pool_count)。

  • 常量池的计数从1开始,即:常量项 = 常量池计数 - 1
  • Class文件格式规范刻意将第0项常量空出,索引值0表示“不引用任何常量池项”

2、常量类型

常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)

  • 字面量: 比较接近于Java语言层面的常量概念,如数值、文本字符串、final常量等
  • 符号引用: 符号引用则属于编译原理方面的概念,包括以下几类常量:
    • 被模块导出或者开放的包(Package)
    • 类和接口的全限定名(Fully Qualified Name)
    • 字段的名称和描述符(Descriptor)
    • 方法的名称和描述符
    • 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
    • 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)

不同于C/C++编译时有“连接”步骤,JVM在加载Class文件时才进行“动态连接”。 因此,Class文件不保存方法、字段最终在内存中的布局信息。 JVM在类加载时从常量池获取符号引用,并在类创建或运行时解析为具体地址。

3、常量表结构

JVM虚拟机规范第四章-常量池open in new window 定义了constant_pool表条目具有以下通用格式:

cp_info {
  u1 tag;
  u1 info[];
}

常量池中的每项常量都是一个表,起始的第一位是一个u1类型的标志位(tag),表示当前常量的类型。

  1. 最初设计的11种常量类型
  2. 为支持动态语言,增加了4种动态语言相关的常量
  3. 为支持Java模块化系统(Jigsaw),新增了CONSTANT_Module_infoCONSTANT_Package_info
类型(tag)描述结构细节(起始u1 tag;
CONSTANT_Utf8_info(1)UTF-8编码的字符串u2 length;
字符串的字节长度
u1 bytes[length];
UTF-8编码的字节数据
CONSTANT_Integer_info(3)整型字面量u4 bytes; 32位整数值
CONSTANT_Float_info(4)浮点型字面量u4 bytes; 32位浮点数值
CONSTANT_Long_info(5)长整型字面量u4 high_bytes; 高32位
u4 low_bytes; 低32位
CONSTANT_Double_info(6)双精度浮点型字面量u4 high_bytes; 高32位
u4 low_bytes; 低32位
CONSTANT_Class_info(7)类或接口的符号引用u2 name_index;
指向类或接口名称的索引
CONSTANT_String_info(8)字符串类型字面量u2 string_index;
指向字符串字面量的索引
CONSTANT_Fieldref_info(9)字段的符号引用u2 class_index;
指向字段所在类的索引
u2 name_and_type_index;
指向字段名称和描述符的索引
CONSTANT_Methodref_info(10)类中方法的符号引用u2 class_index;
指向方法所在类的索引
u2 name_and_type_index;
指向方法名称和描述符的索引
CONSTANT_InterfaceMethodref_info(11)接口中方法的符号引用u2 class_index;
指向接口所在类的索引
u2 name_and_type_index;
指向方法名称和描述符的索引
CONSTANT_NameAndType_info(12)字段或方法的部分符号引用u2 name_index;
指向字段或方法名称的索引
u2 descriptor_index;
指向字段或方法描述符的索引
CONSTANT_MethodHandle_info(15)表示方法句柄u1 reference_kind;
方法句柄的类型
u2 reference_index;
指向方法句柄引用的索引
CONSTANT_MethodType_info(16)表示方法类型u2 descriptor_index;
指向方法类型描述符的索引
CONSTANT_Dynamic_info(17)表示一个动态计算常量u2 bootstrap_method_attr_index;
指向引导方法属性的索引
u2 name_and_type_index;
指向名称和描述符的索引
CONSTANT_InvokeDynamic_info(18)表示一个动态方法调用点u2 bootstrap_method_attr_index;
指向引导方法属性的索引
u2 name_and_type_index;
指向名称和描述符的索引
CONSTANT_Module_info(19)表示一个模块u2 name_index;
指向模块名称的索引
CONSTANT_Package_info(20)表示一个模块中开放或者导出的包u2 name_index;
指向包名称的索引

常量池的数据结构复杂,因为包含17种独立的常量类型,彼此没有共性,因此需要逐项讲解。

访问标志

访问标志: 紧随常量池之后,占2个字节,表示类或接口的访问权限和属性,如是否为publicabstractfinal等。

  • 访问标志不仅用于描述类或接口,在字段表和方法表中也存在各自的访问标志。

类和接口的访问标志(access_flags)

标志名称标志值含义
ACC_PUBLIC0x0001是否为 public 类型
ACC_FINAL0x0010是否被声明为 final,只有类可设置
ACC_SUPER0x0020是否允许使用 invokespecial 字节码指令的新语义,invokespecial 指令的语义在 JDK 1.0.2 发生过改变,为了区别该指令使用哪种语义,JDK 1.0.2 之后编译出来的类的这个标志都必须为真
ACC_INTERFACE0x0200标识这是一个接口
ACC_ABSTRACT0x0400是否为 abstract 类型,对于接口或抽象类来说,此标志值为真,其他类型值为假
ACC_SYNTHETIC0x1000标识这个类并非由用户代码产生的
ACC_ANNOTATION0x2000标识这是一个注解
ACC_ENUM0x4000标识这是一个枚举
ACC_MODULE0x8000标识这是一个模块

总共有 16 个标记位可供使用,但常用的只有其中 7 个,见下图:

16个访问标记位
16个访问标记位

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

类索引、父类索引与接口索引集合: 用于确定类的继承和实现关系,分别指出当前类,父类,以及所实现的接口。

  • 类索引(this_class): 当前类的全限定名
  • 父类索引(super_class): 父类的全限定名
  • 接口索引集合(interfaces): 当前类实现的所有接口

对于接口索引集合,首项为u2类型的接口计数器interfaces_count,表示索引表的容量。 如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。 否则每个索引指向常量池中的一个CONSTANT_Class_info类型的接口名称

类索引查找全限定名的过程

类索引和父类索引都是u2类型的索引值,指向CONSTANT_Class_info常量,再通过CONSTANT_Class_info常量中的索引值,找到定义在CONSTANT_Utf8_info中的全限定名字符串

类索引查找全限定名的过程
类索引查找全限定名的过程

字段表集合

字段表(field_info): 描述类或接口中声明的字段。

JVM虚拟机规范第四章-字段表open in new window 定义了结构:

field_info {
  u2             access_flags;
  u2             name_index;
  u2             descriptor_index;
  u2             attributes_count;
  attribute_info attributes[attributes_count];
}

1、字段访问标志(access_flags

与类中的access_flags类似,都是一个u2的数据类型,取值如下表:

标志名称标志值含义
ACC_PUBLIC0x0001字段是否 public
ACC_PRIVATE0x0002字段是否 private
ACC_PROTECTED0x0004字段是否 protected
ACC_STATIC0x0008字段是否 static
ACC_FINAL0x0010字段是否 final
ACC_VOLATILE0x0040字段是否 volatile
ACC_TRANSIENT0x0080字段是否 transient
ACC_SYNTHETIC0x1000字段是编译器自动产生
ACC_ENUM0x4000字段是否 enum

受Java语法规则的约束:

  • publicprivateprotected 只能三选一
  • finalvolatile不能同时选择
  • 接口中的字段必须有 publicstaticfinal

2、简单名称(name_index)和描述符(descriptor_index

跟随access_flags标志之后的两项索引值;以及全限定名这三种特殊字符串的概念解释:

  • 全限定名:表示字段或方法在类中的完整路径,包括包名和类名。例如java.lang.String
  • 简单名称:表示字段或方法的名称。例如name是字段的简单名称,toString是方法的简单名称
  • 描述符:表示字段或方法的类型信息
    • 对于字段,描述符表示字段的类型,例如I表示int类型
    • 对于方法,描述符表示方法的参数和返回类型,例如(I)V表示接受int参数且无返回值的方法

描述符标识字符含义

标识字符含义
B基本类型 byte
C基本类型 char
D基本类型 double
F基本类型 float
I基本类型 int
J基本类型 long
S基本类型 short
Z基本类型 boolean
V特殊类型 void
L对象类型,例如 Ljava/lang/Object;

3、属性表集合

Class文件、字段表、和方法表都包含各自的属性表集合,用于记录特定场景下的附加信息。 每个属性表集合由属性计数(attributes_count)和若干属性信息(attribute_info)组成。

  • 属性计数 (attributes_count): 表示该集合中包含的属性个数
  • 属性信息 (attribute_info): 每个属性的信息结构,提供详细的元数据

方法表集合

方法表(method_info): 描述类或接口中声明的方法。

JVM虚拟机规范第四章-方法表open in new window 定义了结构,与属性表相似:

method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

1、方法访问标志(access_flags

  • 去除volatiletransient关键字,不能修饰方法
  • 新增synchronizednativestrictfpabstract关键字
标志名称标志值含义
ACC_PUBLIC0x0001方法是否为 public
ACC_PRIVATE0x0002方法是否为 private
ACC_PROTECTED0x0004方法是否为 protected
ACC_STATIC0x0008方法是否为 static
ACC_FINAL0x0010方法是否为 final
ACC_SYNCHRONIZED0x0020方法是否为 synchronized
ACC_BRIDGE0x0040方法是否是由编译器产生的桥接方法
ACC_VARARGS0x0080方法接受不定参数
ACC_NATIVE0x0100方法是否为 native
ACC_ABSTRACT0x0400方法是否为 abstract
ACC_STRICT0x0800方法是否为 strictfp
ACC_SYNTHETIC0x1000方法是否由编译器自动产生

2、方法的代码Code

方法的定义可以通过访问标志、名称索引、描述符索引来表达清楚。 而方法的代码,经过javac编译成字节码指令后存放在方法属性表集合中的Code的属性中。

属性表集合

属性表: 用于存储一些额外的信息,如源文件名称、编译器版本等。

  • Class文件、字段表、和方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。
  • 其限制相对宽松,不要求具有严格顺序。编译器可向属性表写入自定义信息,JVM运行时会忽略不认识的属性。

虚拟机规范预定义的属性

属性名称使用位置含义
Code方法表Java代码编译成的字节码指令
ConstantValue字段表final关键字定义的常量值
Deprecated类、方法表、字段表被声明为deprecated的方法和字段
Exceptions方法表方法抛出的异常列表
EnclosingMethod类文件仅当一个类为局部类或者匿名类时才可能拥有此属性,用于标示此类存在的外部方法
InnerClasses类文件内部类列表
LineNumberTableCode属性Java代码的行号与字节码指令的对应关系
LocalVariableTableCode属性方法的局部变量信息
StackMapTableCode属性JDK6新增属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配
Signature类、方法表、字段表JDK5新增属性,用于支持泛型标记下的方法签名。在 Java 语言中,任何类、接口、初始化方法或成员的字段如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则 Signature 属性会记录泛型签名信息。由于 Java 的泛型采用擦除实现,为了能够在泛型擦除后还能确保签名信息,可以通过 Signature 属性记录泛型签名相关信息
SourceFile类文件记录源文件名称
SourceDebugExtension类文件JDK5新增属性,用于存储额外的调试信息。譬如如在 JSP 文件调试时,无法通过 Java 推栈来推导到 JSP 文件的代码。JSR 45 提议的运行时通过插桩机制向虚拟机中的程序提供了一种进行调试的标准机制,使用该属性可以用于存储插桩时额外新增的调试信息
Synthetic类、方法表、字段表标示为编译器自动生成的代码
LocalVariableTypeTableJDK5新增属性,使用扩展的签名标示符,是为了引入泛型方法之后能描述泛型参数的类型而添加
RuntimeVisibleAnnotations类、方法表、字段表JDK5新增属性,为动态注解提供支持。该属性用于指明哪些注解是在运行时(实际在运行时就意味着反射调用)可见的
RuntimeInvisibleAnnotations类、方法表、字段表JDK5新增属性,与 RuntimeVisibleAnnotations 属性作用相反,用于指明哪些注解是在运行时不可见的
RuntimeVisibleParameterAnnotations方法表JDK5新增属性,作用与 RuntimeVisibleAnnotations 属性类似,只不过作用对象为方法参数
RuntimeInvisibleParameterAnnotations方法表JDK5新增属性,作用与 RuntimeInvisibleAnnotations 属性类似,只不过作用对象为方法参数
AnnotationDefault方法表JDK5新增属性,用于记录注解类型元素默认值
BootstrapMethods类文件JDK7新增属性,用于保存 invokedynamic 指令引用的引导方法限定符
RuntimeVisibleTypeAnnotations类、方法表、字段表、Code 属性JDK8新增属性,为实现 JSR 308 中新增的类型注解提供的支持。用于指明哪些注解是在运行时(实际在运行时意味着反射调用)可见的
RuntimeInvisibleTypeAnnotations类、方法表、字段表、Code 属性JDK8新增属性,为实现 JSR 308 中新增的类型注解提供的支持。与 RuntimeVisibleTypeAnnotations 属性作用相反,用于指明哪些注解是在运行时不可见的
MethodParameters方法表JDK8新增属性,用于支持(编译时加上 -parameters 参数)将方法参数名称保存进 Class 文件中,并可运行时获取此数据。此数据可用于方法参数名称(典型的如 IDE 的代码提示)只能通过 Javadoc 中得到
ModuleJDK9新增属性,用于记录一个 Module 的名称以及相关信息(requires、exports、opens、uses、provides)
ModulePackagesJDK9新增属性,用于记录一个模块中所有存在 exports 或者 opens 的包
ModuleMainClassJDK9新增属性,用于指定一个模块的主类
NestHostJDK11新增属性,用于支持嵌套类(Java中的内部类)的成员和访问控制的 API。——宿主类通过此属性知道自己有哪些内部类
NestMembersJDK11新增属性,用于支持嵌套类(Java中的内部类)的成员和访问控制的 API。——宿主类通过此属性知道自己有哪些内部类

JVM虚拟机规范第四章-属性表open in new window 定义了结构:

attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}

对于每个属性,其名称从常量池中引用1个CONSTANT_Utf8_info表示, 通过1个u4attribute_length说明属性值的字节数,属性值的结构完全自定义。

  1. Code属性
  2. Exceptions属性
  3. LineNumberTable属性
  4. LocalVariableTable及LocalVariableTypeTable属性
  5. SourceFile及SourceDebugExtension属性
  6. ConstantValue属性
  7. InnerClasses属性
  8. Deprecated及Synthetic属性
  9. StackMapTable属性
  10. Signature属性
  11. BootstrapMethods属性
  12. MethodParameters属性
  13. 模块化相关属性
  14. 运行时注解相关属性

编译字节码分析(实践)

使用javac Main.java命令,编译生成Main.class文件:

public class Main {

    private int m;

    public int inc() {
        return m + 1;
    }
}

使用WinHexopen in new window(十六进制编辑器) 打开.class文件查看:

CA FE BA BE 00 00 00 3D 00 13 0A 00 02 00 03 07
00 04 0C 00 05 00 06 01 00 10 6A 61 76 61 2F 6C
61 6E 67 2F 4F 62 6A 65 63 74 01 00 06 3C 69 6E
69 74 3E 01 00 03 28 29 56 09 00 08 00 09 07 00
0A 0C 00 0B 00 0C 01 00 04 4D 61 69 6E 01 00 01
6D 01 00 01 49 01 00 04 43 6F 64 65 01 00 0F 4C
69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00
03 69 6E 63 01 00 03 28 29 49 01 00 0A 53 6F 75
72 63 65 46 69 6C 65 01 00 09 4D 61 69 6E 2E 6A
61 76 61 00 21 00 08 00 02 00 00 00 01 00 02 00 
0B 00 0C 00 00 00 02 00 01 00 05 00 06 00 01 00
0D 00 00 00 1D 00 01 00 01 00 00 00 05 2A B7 00
01 B1 00 00 00 01 00 0E 00 00 00 06 00 01 00 00
00 01 00 01 00 0F 00 10 00 01 00 0D 00 00 00 1F
00 02 00 01 00 00 00 07 2A B4 00 07 04 60 AC 00
00 00 01 00 0E 00 00 00 06 00 01 00 00 00 06 00
01 00 11 00 00 00 02 00 12
  • CA FE BA BE魔数,用于标识Class文件格式
  • 00 00 00 3D版本号,其中00 00是次版本号,00 3D是主版本号(61,对应Java17)
  • 00 13常量池计数0x13十进制为19,第0项常量空出,因此常量池中有18个常量

使用javap -verbose Main命令查看常量池

Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // Main.m:I
   #8 = Class              #10            // Main
   #9 = NameAndType        #11:#12        // m:I
  #10 = Utf8               Main
  #11 = Utf8               m
  #12 = Utf8               I
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               inc
  #16 = Utf8               ()I
  #17 = Utf8               SourceFile
  #18 = Utf8               Main.java
  • 00 21访问标志ACC_PUBLIC(public)和ACC_SUPER(super)
类索引查找全限定名的过程
类索引查找全限定名的过程
  • 00 08类索引,指向常量池第8项#8 = Class #10 // Main,表示当前类是Main
  • 00 02父类索引,指向常量池第2项#2 = Class #4 // java/lang/Object,表示父类是Object
  • 00 00接口计数器,为0表示该类没有实现任何接口,所以接口索引集合为空
  • 00 01字段计数器,表示有1个字段
  • 00 02 00 0B 00 0C 00 00: 字段表集合
    • access_flags00 02表示private访问权限
    • name_index00 0B指向常量池中的第11项#11 = Utf8 m,表示字段名为m
    • descriptor_index00 0C指向常量池中的第12项#12 = Utf8 I,表示字段类型为int
    • attributes_count00 00表示没有属性,所以attribute_info为空
  • 00 02方法计数器,表示有2个方法

使用javap -verbose Main命令查看方法表集合, 或使用IDEA jclasslib插件open in new window查看:

{
  public Main();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public int inc();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #7                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 6: 0
}

酷 壳 – CoolShell《实例分析JAVA CLASS的文件结构》open in new window