Class类文件结构
1. 前言
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有任何分隔符,这使得Class文件中存储的内容几乎全部都是程序运行的必要数据
Class文件格式采用类似C语言结构体的伪结构来存储数据,这种结构只有两种数据类型:无符号数和表
- 无符号数
属于基本数据类型,主要可以用来描述数字、索引符号、数量值或者按照UTF-8编码构成的字符串值,以u1、u2、u4、u8分别表示1字节、2字节、4字节和8字节
- 表
由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯以“_info”结尾。
用于描述有层次关系的复合结构的数据,比如方法、字段。需要注意的是class文件是没有分隔符的,所以每个的二进制数据类型都是严格定义的。具体的顺序定义如下:
几个概念:
- 全限定名
将类全名中的“.”替换为“/”,为了保证多个连续的全限定名之间不产生混淆,在最后加上“;”表示全限定名结束。例如:”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类型的标志位,代表这个常量属于哪种属于哪种常量类型,常量类型如下所示:
1 | // 表示类或接口 |
1 | // 表示字符串常量的值 |
2.4 访问标志
access_flags: 常量池之后两个字节,用于识别一些类或接口层次的访问信息
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 | field_info { |
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 | method_info { |
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 | attribute_info { |
与Class文件中其它数据项对长度、顺序、格式的严格要求不同,属性表集合不要求其中包含的属性表具有严格的顺序,并且只要属性的名称不与已有的属性名称重复,任何人实现的编译器可以向属性表中写入自己定义的属性信息。虚拟机在运行时会忽略不能识别的属性,为了能正确解析Class文件,虚拟机规范中预定义了虚拟机实现必须能够识别的9项属性(预定义属性已经增加到21项):
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类文件、字段表、方法表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTale | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述(局部变量作用域) |
SourceFile | 类文件 | 源文件名称 |
Synthetic | 类文件、方法表、字段表 | 标识方法或字段是由编译器自动生成的 |
- Code属性
1 | Code_attribute { |
- LocalVariableTable 属性
LocalVariableTable 是可选变长属性,位于 Code属性的属性表中。它被调试器用于确定方法在执行过程中局部变量的信息。在 Code 属性的属性表中, LocalVariableTable 属性可以按照任意顺序出现。 Code 属性中的每个局部变量最多只能有一 个 LocalVariableTable 属性。
1 | LocalVariableTable_attribute { |
参考
《深入理解Java虚拟机》
《Java虚拟机规范》