Fork me on GitHub

【JVM】Class类文件结构

Class类文件结构

1. 前言

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有任何分隔符,这使得Class文件中存储的内容几乎全部都是程序运行的必要数据

Class文件格式采用类似C语言结构体的伪结构来存储数据,这种结构只有两种数据类型:无符号数和表

  • 无符号数

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

由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯以“_info”结尾。

用于描述有层次关系的复合结构的数据,比如方法、字段。需要注意的是class文件是没有分隔符的,所以每个的二进制数据类型都是严格定义的。具体的顺序定义如下:

1532919736753

几个概念:

  • 全限定名

将类全名中的“.”替换为“/”,为了保证多个连续的全限定名之间不产生混淆,在最后加上“;”表示全限定名结束。例如:”com.test.Test”类的全限定名为”com/test/Test;”

  • 简单名称

没有类型和参数修饰的方法或字段名称。例如:”public void add(int a,int b){…}”该方法的简单名称为”add”,”int a = 123;”该字段的简单名称为”a”

  • 描述符

描述字段的数据类型、方法的参数列表(包括数量、类型和顺序)和返回值。根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符表示,而对象类型则用字符L加对象全限定名表示

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

对于数组类型,每一维将使用一个前置的“[”字符来描述,如:”int[]”将被记录为”[I”,”String[][]”将被记录为”[[Ljava/lang/String;”

用描述符描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组”()”之内,如:方法”String getAll(int id,String name)”的描述符为”(I,Ljava/lang/String;)Ljava/lang/String;”

2. Class文件结构

2.1 魔数

  • 每个Class文件的头4个字节称为魔数(Magic Number)
  • 唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件。
  • Class文件魔数的值为0xCAFEBABE

2.2 版本号

紧接着魔数的4个字节是Class文件版本号,如果Class文件的版本号超过虚拟机版本,将被拒绝执行 。版本号分为:

  • 次版本号(minor_version): 前2字节用于表示次版本号
  • 主版本号(major_version): 后2字节用于表示主版本号。

2.3 常量池

紧接着魔数与版本号之后的是常量池入口,常量池简单理解为class文件的资源从库

  • Class文件结构中与其它项目关联最多的数据类型、
  • 占用Class文件空间最大的数据项目之一
  • 在文件中第一个出现的表类型数据项目

由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。从1开始计数,第0项空出来是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达”不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0来表示。

常量池之中主要存放两大类常量:

  • 字面量: 比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等
  • 符号引用: 属于编译原理方面的概念,包括了下面三类常量:
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符

在Class文件中不会保存各个方法和字段的最终内存布局信息,因此这些字段和方法的符号引用不经过转换的话是无法被虚拟机使用的。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

常量池中每一项都是一种表,共14种,表开始的第一位都是一个u1类型的标志位,代表这个常量属于哪种属于哪种常量类型,常量类型如下所示:

1532920845018

1
2
3
4
5
// 表示类或接口
CONSTANT_Class_info {
u1 tag; //标志位
u2 name_index; //指向常量池的有效索引,常量池在该索引处为CONSTANT_Utf8_info
}
1
2
3
4
5
6
// 表示字符串常量的值
CONSTANT_Utf8_info {
u1 tag;
u2 length; //指明了 bytes[]数组的长度
u1 bytes[length]; //表示字符串值的byte数组
}

2.4 访问标志

access_flags: 常量池之后两个字节,用于识别一些类或接口层次的访问信息

1532921992330

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

  • 类索引(this_class),用于确定这个类的全限定名,占2字节
  • 父类索引(super_class),用于确定这个类父类的全限定名(Java语言不允许多重继承,故父类索引只有一个。除了java.lang.Object类之外所有类都有父类,故除了java.lang.Object类之外,所有类该字段值都不为0),占2字节
  • 接口索引计数器(interfaces_count),占2字节。如果该类没有实现任何接口,则该计数器值为0,并且后面的接口的索引集合将不占用任何字节
  • 接口索引集合(interfaces),一组u2类型数据的集合。用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果该类本身为接口,则为extends语句)后的接口顺序从左至右排列在接口的索引集合

this_class、super_class与interfaces按顺序排列在访问标志之后,它们中保存的索引值均指向常量池中一个CONSTANT_Class_info类型的常量,通过这个常量中保存的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串

2.6 字段表集合

字段表结构为:

1
2
3
4
5
6
7
field_info {
u2 access_flags;//定义字段被访问权限和基础属性
u2 name_index; //表示一个有效的字段的非全限定名,是对常量池的一个有效索引
u2 descriptor_index; //表示一个有效的字段的描述符,是对常量池的一个有效索引
u2 attributes_count; //表示当前字段的附加属性的数量
attribute_info attributes[attributes_count];//属性表集合
}

access_flags 用于定义字段被访问权限和基础属性的掩码标志,取值如下:

名称 标志值 含义
ACC_PUBLIC 0x0001 public,表示字段可以从任何包访问。
ACC_PRIVATE 0x0002 private,表示字段仅能该类自身调用。
ACC_PROTECTED 0x0004 protected,表示字段可以被子类调用。
ACC_STATIC 0x0008 static,表示静态字段。
ACC_FINAL 0x0010 final, 表示字段定义后值无法修改。
ACC_VOLATILE 0x0040 volatile, 表示字段是易变的。
ACC_TRANSIENT 0x0080 transient, 表示字段不会被序列化。
ACC_SYNTHETIC 0x1000 表示字段由编译器自动产生。
ACC_ENUM 0x4000 enum, 表示字段为枚举类型。

2.7 方法表集合

定义所有方法,包括实例初始化方法和类初始化方法,方法表结构如下:

1
2
3
4
5
6
7
method_info {
u2 access_flags; //定义当前方法的访问权限和基本属性的掩码标志
u2 name_index; //表示一个方法的有效的非全限定名
u2 descriptor_index; //表示一个有效的方法的描述符
u2 attributes_count; //表示方法的附加属性的数量
attribute_info attributes[attributes_count]; //方法属性
}

access_flags 项的值是用于定义当前方法的访问权限和基本属性的掩码标志, 取值如下:

标记名 说明
ACC_PUBLIC 0x0001 public, 方法可以从包外访问
ACC_PRIVATE 0x0002 private, 方法只能本类中访问
ACC_PROTECTED 0x0004 protected, 方法在自身和子类可以访问
ACC_STATIC 0x0008 static, 静态方法
ACC_FINAL 0x0010 final, 方法不能被重写(覆盖)
ACC_SYNCHRONIZED 0x0020 synchronized, 方法由管程同步
ACC_BRIDGE 0x0040 bridge, 方法由编译器产生
ACC_VARARGS 0x0080 表示方法带有变长参数
ACC_NATIVE 0x0100 native, 方法引用非 java 语言的本地方法
ACC_ABSTRACT 0x0400 abstract, 方法没有具体实现
ACC_STRICT 0x0800 strictfp, 方法使用 FP-strict 浮点格式
ACC_SYNTHETIC 0x1000 方法在源文件中不出现,由编译器产生

方法中的Java代码,经编译器编译成字节码指令后,保存在方法属性表集合中的”Code”属性中

2.8 属性表集合

在CLass文件,字段表,方法表都可携带自己的属性表集合,以用于描述某些场景专有的信息,属性表的结构如下:

1
2
3
4
5
attribute_info {
u2 attribute_name_index; //指向常量池的无符号索引。常量池在该索引处的项必须是 CONSTANT_Utf8_info
u4 attribute_length; //info[attribute_length] 长度
u1 info[attribute_length]; //属性值
}

与Class文件中其它数据项对长度、顺序、格式的严格要求不同,属性表集合不要求其中包含的属性表具有严格的顺序,并且只要属性的名称不与已有的属性名称重复,任何人实现的编译器可以向属性表中写入自己定义的属性信息。虚拟机在运行时会忽略不能识别的属性,为了能正确解析Class文件,虚拟机规范中预定义了虚拟机实现必须能够识别的9项属性(预定义属性已经增加到21项):

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 final关键字定义的常量值
Deprecated 类文件、字段表、方法表 被声明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常
InnerClasses 类文件 内部类列表
LineNumberTale Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述(局部变量作用域)
SourceFile 类文件 源文件名称
Synthetic 类文件、方法表、字段表 标识方法或字段是由编译器自动生成的
  • Code属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack; //当前方法的操作数栈在运行执行的任何时间点的最大深度
u2 max_locals; //分配在当前方法引用的局部变量表中的局部变量个数,包括调用此方法时用于传递参数的局部变量;long 和 double 型的局部变量的最大索引是 max_locals-2,其它类型的局部变量的最大索引是 max_locals-1
u4 code_length; //当前方法的 code[]数组的字节数
u1 code[code_length]; //实现当前方法的 Java 虚拟机字节码
u2 exception_table_length;
{ u2 start_pc; //start_pc 和 end_pc 两项的值表明了异常处理器在 code[]数组中的有效范围
u2 end_pc;
u2 handler_pc; //表示一个异常处理器的起点
u2 catch_type; //指向常量池的一个有效索引, CONSTANT_Class_info类型
} exception_table[exception_table_length]; //表示 code[]数组中的一个异常处理器
u2 attributes_count;
attribute_info attributes[attributes_count]; //属性表的每个成员的值必须是 attribute 结构
}
  • LocalVariableTable 属性

LocalVariableTable 是可选变长属性,位于 Code属性的属性表中。它被调试器用于确定方法在执行过程中局部变量的信息。在 Code 属性的属性表中, LocalVariableTable 属性可以按照任意顺序出现。 Code 属性中的每个局部变量最多只能有一 个 LocalVariableTable 属性。

1
2
3
4
5
6
7
8
9
10
11
LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{ u2 start_pc; //局部变量的索引都在范围[start_pc, start_pc+length)中
u2 length;
u2 name_index;//表示一个局部变量的有效的非全限定名, 常量池的一个有效索引,CONSTANT_Utf8_info结构
u2 descriptor_index;//表示源程序中局部变量类型的字段描述符, CONSTANT_Utf8_info结构
u2 index;//局部变量在当前栈帧的局部变量表中的索引
} local_variable_table[local_variable_table_length];
}

参考

《深入理解Java虚拟机》

《Java虚拟机规范》

https://www.cnblogs.com/wade-luffy/p/5929325.html