目录
类文件结构
6.1 概述
6.2 无关性的基石
6.3 Class类文件的结构
注意 任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)。本章中,笔者只是通俗地将任意一个有效的类或接口所应当满足的格式成为“Class文件格式”,实际上它并不一定以磁盘文件的形式存在。
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表,后面的解析都要以这两种数据类型为基础。
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,它由表6-1所示的数据项构成。
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u4 | magic | 1 | 魔数:确定一个文件是否是Class文件 |
u2 | minor_version | 1 | Class文件的次版本号 |
u2 | major_version | 1 | Class文件的主版本号:一个JVM实例只能支持特定范围内版本号的Class文件(可以向下兼容)。 |
u2 | constant_pool_count | 1 | 常量表数量 |
cp_info | constant_pool | constant_pool_count-1 | 常量池:可以理解为Class文件的资源仓库,后面的其他数据项可以引用常量池内容。 |
u2 | access_flags | 1 | 类的访问标志信息:用于表示这个类或者接口的访问权限及基础属性。 |
u2 | this_class | 1 | 指向当前类的常量索引:用来确定这个类的的全限定名。 |
u2 | super_class | 1 | 指向父类的常量的索引:用来确定这个类的父类的全限定名。 |
u2 | interfaces_count | 1 | 接口的数量 |
u2 | interfaces | interfaces_count | 指向接口的常量索引:用来描述这个类实现了哪些接口。 |
u2 | fields_count | 1 | 字段表数量 |
field_info | fields | fields_count | 字段表集合:描述当前类或接口声明的所有字段。 |
u2 | methods_count | 1 | 方法表数量 |
method_info | methods | methods_count | 方法表集合:只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。 |
u2 | attributes_count | 1 | 属性表数量 |
attribute_info | attributes | attributes_count | 属性表集合:用于描述某些场景专有的信息,如字节码的指令信息等等。 |
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。
6.3.1 魔数与Class文件的版本
每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的Class文件。0xCAFEBABE(咖啡宝贝?)
紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1(JDK1.0~1.1使用了45.0~45.3的版本号),高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
代码清单6-1使用JDK1.6编译输出Class文件。
package club.limeyu.classloading;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/14 下午2:22
*/
public class TestClass {
private int m;
public int inc() {
return m + 1;
}
}
代码清单6-1对应的Class文件(%!xxd)
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
0000000: ca fe ba be 00 00 00 32 00 13 0a 00 04 00 0f 09 .......2........
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
0000010: 00 03 00 10 07 00 11 07 00 12 01 00 01 6d 01 00 .............m..
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
0000020: 01 49 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 .I...<init>...()
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
0000030: 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e V...Code...LineN
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
0000040: 75 6d 62 65 72 54 61 62 6c 65 01 00 03 69 6e 63 umberTable...inc
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
0000050: 01 00 03 28 29 49 01 00 0a 53 6f 75 72 63 65 46 ...()I...SourceF
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
0000060: 69 6c 65 01 00 0e 54 65 73 74 43 6c 61 73 73 2e ile...TestClass.
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
0000070: 6a 61 76 61 0c 00 07 00 08 0c 00 05 00 06 01 00 java............
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
0000080: 1d 6f 72 67 2f 66 65 6e 69 78 73 6f 66 74 2f 63 .org/fenixsoft/c
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
0000090: 6c 61 7a 7a 2f 54 65 73 74 43 6c 61 73 73 01 00 lazz/TestClass..
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000a0: 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 .java/lang/Objec
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000b0: 74 00 21 00 03 00 04 00 00 00 01 00 02 00 05 00 t.!.............
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000c0: 06 00 00 00 02 00 01 00 07 00 08 00 01 00 09 00 ................
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000d0: 00 00 1d 00 01 00 01 00 00 00 05 2a b7 00 01 b1 ...........*....
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000e0: 00 00 00 01 00 0a 00 00 00 06 00 01 00 00 00 07 ................
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000f0: 00 01 00 0b 00 0c 00 01 00 09 00 00 00 1f 00 02 ................
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
0000100: 00 01 00 00 00 07 2a b4 00 02 04 60 ac 00 00 00 ......*....`....
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
0000110: 01 00 0a 00 00 00 06 00 01 00 00 00 0b 00 01 00 ................
0000120: 0d00 0000 0200 0e0a ........
魔数
u4 CA FE BA BE
次版本
u2 00 00
主版本
u2 00 32
常量项数量
u2 00 13 = 19 - 1 = 18
第一项常量
u1 0a CONSTANT_Methodref_info
u2 00 04 #4 :: #18 :: (java/lang/Object) 指向声明方法的类的描述符的CONSTANT_Class_info的索引项
u2 00 0f #15 :: #7:#8 :: (<init>:()V) 指向名称及类型描述符CONSTANT_NameAndType的索引项
第二项常量
u1 09 CONSTANT_Fieldref_info
u2 00 03 #3 :: #17 :: (org/fenixsoft/clazz/TestClass)指向声明字段的类或接口描述符CONSTANT_Class_info的索引项
u2 00 10 #16 :: #5:#6 :: (m:I)指向字段描述符CONSTANT_NameAndType_info 的索引项
第三项常量
u1 07 CONSTANT_Class_info
u2 00 11 #17 (org/fenixsoft/clazz/TestClass) 指向全限定名常量项的索引
第四项常量
u1 07 CONSTANT_Class_info
u2 00 12 #18 :: (java/lang/Object) 指向全限定名常量项的索引
第五项常量
u1 01 CONSTANT_Utf8_info
u2 00 01
u1 6d (m)
第六项常量
u1 01 CONSTANT_Utf8_info
u2 00 01
u1 49 (I)
第七项常量
u1 01 CONSTANT_Utf8_info
u2 00 06
u1 3c 69 6e 69 74 3e (<init>)
第八项常量
u1 01 CONSTANT_Utf8_info
u2 00 03
u1 28 29 56 (()V)
第九项常量
u1 01 CONSTANT_Utf8_info
u2 00 04
u1 43 6f 64 65 (Code)
第十项常量
u1 01 CONSTANT_Utf8_info
u2 00 0f
u1 4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 01 (LineNumberTable)
十一项常量
u1 01 CONSTANT_Utf8_info
u2 00 03
u1 69 6e 63 (inc)
十二项常量
u1 01 CONSTANT_Utf8_info
u2 00 03
u1 28 29 49 (()I)
十三项常量
u1 01 CONSTANT_Utf8_info
u2 00 0a
u1 53 6f 75 72 63 65 46 69 6c 65 (SourceFile)
十四项常量
u1 01 CONSTANT_Utf8_info
u2 00 0e
u1 54 65 73 74 43 6c 61 73 73 2e 6a 61 76 61 (TestClass.Java)
十五项常量
u1 0c CONSTANT_NameAndType_info
u2 00 07 #7 :: (<init>)指向该字段或方法名称常量项的索引
u2 00 08 #8 :: (()V)指向该字段或方法描述符常量项的索引
十六项常量
u1 0c CONSTANT_NameAndType_info
u2 00 05 #5 :: (m) 指向该字段或方法名称常量项的索引
u2 00 06 #6 :: (I) 指向该字段或方法描述符常量项的索引
十七项常量
u1 01 CONSTANT_utf8_info
u2 00 1d
u1 6f 72 67 2f 66 65 6e 69 78 73 6f 66 74 2f 63 6c 61 7a 7a 2f 54 65 73 74 43 6c 61 73 73 (org/fenixsoft/clazz/TestClass)
十八项常量
u1 01 CONSTANT_Utf8_info
u2 00 10
u1 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 (java/lang/Object)
访问标志
u2 00 21 0x0001 | 0x0020 = ACC_PUBLIC | ACC_SUPER
类索引
u2 00 03 #3 :: #17 (org/fenixsoft/clazz/TestClass)
父索引
u2 00 04 #4 :: #18 :: (java/lang/Object)
接口索引集合
u2 00 00
字段表集合
u2 00 01
字段表
u2 00 02 ACC_PRIVATE access_flags
u2 00 05 #5 (m) name_index 简单名称
u2 00 06 #6 (I) descriptor_index 字段的数据类型
u2 00 00 attributes_count
attribute_info attributes
方法表集合
u2 00 02
方法表
第一个方法
u2 00 01 ACC_PUBLIC access_flags
u2 00 07 #7 (<init>) name_index
u2 00 08 #8 (()V) descriptor_index
u2 00 01 attributes_count
u2 00 09 #9 (Code) attribute_name_index
u4 00 00 00 1d attribute_count 1 * 16 + 13 = 29
u2 00 01 max_stack
u2 00 01 max_locals
u4 00 00 00 05 code_length
u1 2a b7 00 01 b1 code (aload_0 invokespecial 00 01(#1) return)
u2 00 00 exception_table_length
u2 00 01 attribute_count
u2 00 0a #10 (LineNumberTable) attribute_name_index
u4 00 00 00 06 attribute_length
u2 00 01 line_number_table_length
u2 00 00 start_pc
u2 00 07 line_number
第二个方法
u2 00 01 ACC_PUBLIC access_flags
u2 00 0b name_index #11 (inc)
u2 00 0c descriptor_index #12 (()I)
u2 00 01 attributes_count
u2 00 09 attribute_name_index #9 (Code)
u4 00 00 00 1f attribute_length 1 * 16 + 15 = 31
u2 00 02 max_stack
u2 00 01 max_locals
u4 00 00 00 07 code_length
u1 2a b4 00 02 04 60 ac code (aload_0 getfield 00 02(#2) iconst_1 iadd iteturn)
u2 00 00 exception_table_length
u2 00 01 attribute_count
u2 00 0a attribute_name_index #10 (LineNumberTable)
u4 00 00 00 06 attribute_length
u2 00 01 line_number_table_length
u2 00 00 start_pc
u2 00 0b line_number
图6-2显示的是使用十六进制打开这个Class文件的结果,可以清楚地看见开头4个字节的十六进制表示是0xCAFEBABE,代表次版本号的第5和第6个字节值为0x0000,而主版本号的值为0x0032,也即是十进制的50,该版本号说明这个文件是可以被JDK1.6或以上版本虚拟机执行的Class文件。

6.3.2 常量池
紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池常量计数值(constant_pool_count)。与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的,如图6-3所示,常量池容量(偏移地址:0x00000008)为十六进制数0x0016,即十进制的22,这就代表常量池中有21项常量,索引值范围为1~21。在Class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的是在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0来表示。Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始的。

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。关于类的创建和动态连接内容,在下一章介绍虚拟机类加载过程时在进行详细讲解。
常量池中每一项常量都是一个表,在JDK 1.7之前共有11中结构各不相同表结构数据,在JDK 1.7 中为了更好地支持动态语言调用,又额外增加了3种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info,本章不会涉及这3中新增的类型,在第8章介绍字节码执行和方法调用时,将会详细讲解)。
这14种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位(tag,取值见表6-3中标识列),代表当前这个常量属于那种常量类型。这14种常量类型所代表的具体含义见表6-3。
类型 | 标志 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型的字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 标识方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
6.3.3 访问标志
6.3.4 类索引、父类索引与接口索引集合
6.3.5 字段表集合
6.3.6 方法表集合
6.3.7 属性表集合
1. Code属性
Java程序方法体中代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性。譬如接口或者抽象类中的方法就不存在Code属性,如果方法表有Code属性存在,那么它的结构将如表6-15所示。
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为“Code”,它代表了该属性的属性名称,attribute_length 指示了属性值的长度,由于属性名称索引与属性长度一共为6字节,所以属性值的长度固定为整个属性表长度减去6个字节。
max_stack代表了操作数栈(Operand Stacks)深度的最大值。在方法执行的任意时刻,
max_locals代表了局部变量表所需的存储空间。在这里,max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。对于boolean、byte、char、short、int、float、reference和returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这两种64位的数据类型则需要两个Slot来存放。方法参数(包括实例方法中的隐藏参数“this”)、显示异常处理器的参数(Exception Handler Parameter,就是try-catch语句中catch块所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放。另外,并不是在方法中用到了多少局部变量,就把这些局部变量所占Slot之和作为max_locals的值,原因是局部变量表中的Slot可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的Slot可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配Slot给各个变量使用,然后计算出max_locals的大小。
code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用来存储字节码指令的一系列字节流。既然叫字节码指令,
关于code_length,
Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。了解Code属性是学习后面关于字节码执行引擎内容的必要基础,能直接阅读字节码也是工作中分析Java代码语义问题的必要工具和基本技能,因此笔者准备了一个比较详细的实例来讲解虚拟机是如何使用这个属性的。
继续以代码清单6-1的TestClass.class文件为例,如图6-10所示,这是上一节分析过的实例构造器“<init>”方法的Code属性。它的操作数的最大深度和本地变量表的容量都为0x0001,字节码区域所占空间的长度为0x0005。虚拟机读取到字节码区域的长度后,按照顺序一次读入紧随的5个字节,并根据字节码指令表翻译出所对应的字节码指令。翻译“2A B7 00 01 B1”的过程为:
1)读入2A,查表得0x2A对应的指令为aload_0,这个指令的含义是将第0个Slot中为reference类型的本地变量推送到操作数栈顶。
2)读入B7,
3)读入00 01,
4)读入B1

这段字节码虽然很短,
我们再次使用javap命令把此Class文件的另一个方法的字节码指令也计算出来,结果如代码清单6-4所示。

2. Exceptions属性
3. LineNumberTable属性
LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,但是默认会生成到Class文件之中,可以在Javac中分别使用-g:none或-g:lines选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。LineNumberTable属性的结构见表6-18。
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | line_number_table_length | 1 |
line_number_info | line_number_table | line_number_table_length |
line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合,line_number_info表包括了start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号。
4.LocalVariableTable属性
5.SourceFile属性
6.ConstantValue属性
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。