在了解Java class文件结构之前,我们思考一下如下几个问题:
- Windows、Linux、Mac 三大平台上的可执行程序格式一样吗?如果不一样,那是什么原因导致的哪?
- png、jpeg等格式的图片在上述三个平台上都能打开,原因是什么?
- java文件被编译成class文件,结合Java语法,你觉得class文件中应该包含哪些信息哪?
一. 无关性的基石
Java实现了程序的“一次编写,到处运行”,不管在哪种操作平台上,虚拟机都可以载入和执行同一种字节码文件,这种字节码文件是与平台无关的。
各种不同平台的虚拟机与所有平台都统一使用的程序存储格式--字节码是构成平台无关性的基石。
对虚拟机而言,它是语言无关的,它并不和包括Java在内的任何语言绑定,它只与“class文件”这种特定的二进制文件格式相关联,class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。任一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的class文件,虚拟机并不关心class的来源是何种语言。
二. Class类文件的结构
class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在class文件之中,中间没有添加任何分隔符,这使得整个class文件中存储的内容几乎全部都是程序运行的必要数据,没有空隙存在。
根据Java虚拟机规范的规定,class文件格式采用一种类似C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值以及按照UTF-8编码构成字符串值。
表是由无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个class文件本质上就是一张表,它的数据项如下图所示:
类型 | 名称 | 数量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count - 1 |
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 |
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这一系列连续的某一类型的数据被称为某一类型的集合。
1. 魔数与class文件的版本
每个class文件的头4个字节称为魔数(Magic Number),它的唯一作用就是确定这个文件是否为一个能被虚拟机接受的class文件,也即是用来确定文件格式,同jpeg、gif等文件格式一样。实际上,很多文件都用Magic Number来标识文件格式。
Magic Number后的4个字节存储的是class文件的版本号:5~6字节是次版本号,7~8字节是主版本号。
2. 常量池
常量池是class文件的“资源仓库”,是class文件结构中与其他项目关联最多的数据类型,也是占用class文件空间最大的数据项目之一。
常量池中常量的数量是不固定的,所以在常量池入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。
3. 访问标志
访问标志(access_flags)用于识别一些类或者接口层次的访问信息,包括:这个class是类还是接口,是否定义为public类型,是否定义为abstract类型,是类的话是否被声明为final等。
access_flags中一共有16个标志位可以使用,当前只定义了其中8个,没有使用到的标志位一律为0。
4. 类索引、父类索引、接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据。
接口索引集合(interfaces)是一组u2类型数据的集合。
class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。Java语言不允许多重继承,所以父类索引只有1个。接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。
类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。
对于接口索引集合,入口的第一项--u2类型的数据为接口计数器(interfaces_count),表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。
5. 字段表集合
字段表用于描述接口或类中声明的变量。字段表包含的信息有:
- 字段的作用域(public、private、protected)
- 是实例变量还是类变量(static修饰符)
- 可变性(final)
- 并发可见性(volatile修饰符)
- 可否被实例化(transient修饰符)
- 字段数据类型(基本类型、对象、数组)
- 字段名称
上述信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,适合使用标志位来表示。
字段名称、字段数据类型是无法固定的,只能引用常量池中的常量来描述。
6. 方法表集合
方法表的内容同字段表的内容一样,它们不同的地方在于访问标志和属性表集合的可选项。比如:volatile和transient关键字不能修饰方法,所以方法表的访问标志中就没有ACC_VOLATILE和ACC_TRANSIENT标志;synchronized、native、strictfp和abstract关键字可以修饰方法,所以方法表的访问标志中增加了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志。
方法的定义通过访问标志、名称索引、描述符索引表表达,而方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里面。
7. 属性表集合
在Class文件、字段表、方法表中都可以携带自己的属性表(attribute_info)集合,用来描述某些场景专有的信息。
与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略它不认识的属性。Java SE 7中预定义了21项属性,每种属性都有自己的表结构。