Class文件是Java程序跨平台的保证,正是由于有了Class文件架起源码和机器码之间的中间桥梁,JVM虚拟机才可以在各种平台上按照统一的规范标准加载Java代码。
作为“写给虚拟机看的”Java代码,Class文件结构必须设计得足够完善,同时由于Java虚拟机规范并不只针对Java,Class文件又不能引入过多细节。本篇博客我们就来介绍下Class文件的结构。
一个Class文件对应一个Java Class,所以一个Class文件记录着一个类的全部信息,JVM通过Class文件将对应的类加载入内存。
Class文件的结构主要分为以下几部分:
- 魔数
- 常量池
- 访问标识
- 类索引、父类索引、接口索引
- 字段表集合
- 方法表集合
- 索引表集合
1 魔数
每个Class文件的头4个字节成为魔数(Magic Number),它的唯一作用就是确定这个文件是否能作为一个Class文件被接受。很多文件都以魔数进行类型识别,如gif、jpeg等图片文件。之所以使用魔数而不是扩展名是处于安全考虑,文件扩展名可以所以改动。Class文件的魔数是0xCAFEBABE。
紧接着魔数的4个字节存储的是Class文件的版本号,5、6字节为次版本号,7、8字节为主版本号。不同版本的虚拟机可以接受不同版本的class文件,所以虚拟机通过主次版本号判断是否可以加载目标class文件。
2 常量池
常量池可以看做是Class文件的资源仓库,也是Class文件中占用空间最大的部分。常量池主要存放两大类常量:字面量、符号引用。
字面量比较接近Java语言层面的常量,如文本字符串、生命为final的常量等。
符号引用属于编译范畴中的概念,主要包括三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
Java语言不同于C、C++等语言在编译阶段即进行链接,相应的链接都放到了运行时阶段。所以Class文件中不可能包含各个方法、字段在内存中的布局。Java虚拟机在运行阶段加载类时,将符号引用转换成真正的内存入口地址,对应类才算可以工作。
常量池中的每一项代表一个常量,JDK目前共有14中类型的常量,而每一个常量又有自己的内部结构。类或接口符号索引是其中较为简单的一项,接下来以类索引为例做简单介绍。类符号索引对应的类型为CONSTANT_Class_info,其结构如下:
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | name_index | 1 |
tag是标志位,表明类型。CONSTANT_Class_info的tag为7。name_index是一个索引值,它指向常量池中一个CONSTANT_Utf8_info类型常量,此常量代表了这个类的全限定名。
CONSTANT_Utf8_info的结构如下所示:
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
bytes字段的内容就是类的全限定名。
3 访问标志
常量池之后的两个字节代表访问标志(accss_flags),用于识别类或接口的层次访问信息:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public |
ACC_FINAL | 0x0010 | 是否被声明为final |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义 |
ACC_INTERFACE | 0x0200 | 是否为接口 |
ACCS_ABSTRACT | 0x0400 | 是否为abstract类型 |
ACC_SYNTHETIC | 0x1000 | 标示该类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标示这是一个注解 |
ACC_ENUM | 0x4000 | 标示这是一个枚举 |
4 类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据集合。Class文件中的这三项决定了类的继承关系。
类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个CONSTANT_Class_info类描述符常量,通过CONSTANT_Class_info类型的常量索引值可以找到定义在CONSTAN_Utf8_info类型的常量中的类全限定名。
对于接口索引集合,入口的第一项u2类型的数据为接口计数器(interfaces_count)表示索引表的容量。每个接口的同样由一个u2类型数据指向一个CONSTANT_Class_info。
5 字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量和实例级变量,但不包括定义在方法内部的局部变量。每个字段的结构如下图所示:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attribute_count |
5.1 访问标识
标志名称 | 含义 |
---|---|
ACC_PUBLIC | 是否为public |
ACC_PRIVATE | 是否为private |
ACC_PROTECTED | 是否为protected |
ACC_STATIC | 是否为static |
ACC_FINAL | 是否为final |
ACC_VOLATILE | 是否为volatile |
ACC_TRANSIENT | 是否为transient |
ACC_SYNTHETIC | 是否为编译器自动产生 |
ACC_ENUM | 是否为enum |
字段的访问标识access_flags与类访问标识类似。
5.2 name_index
name_index标识字段的简单名称。简单名称和全限定名的区别在于:全限定名是类的全路径名,如org/fenixsoft/clazz/TestClass,只是把类全名中的"."替换成“/”而已。简单名称指的是没有类型和参数修饰的方法或者字段名称,如一个类中含有一个字段"m",则其简单名称为"m"。
5.3 descriptor_index
descriptor_index为字段或方法的描述符。描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。基本数据类型以及代表无返回值的void以及对象类型均由一个大写字符来代替:
标志字段 | 含义 |
---|---|
B | byte |
C | char |
D | double |
F | float |
I | int |
J | long |
S | short |
Z | boolean |
V | void |
L | 对象类型,如Ljava/lang/object |
对于数组类型,每一个维度用一个"["来描述,比如定义一个“java.lang.String[][]”类型的二维数组,将被记录为“[[Ljava/lang/string”。
方法描述符按照先参数列表后返回值的顺序描述,参数列表按照参数顺序放在一组"()"之内。如方法int indexOf(char[] source, int sourceOffest, int sourceCount, char[] target, int targetOffest, int targetCount, int fromIndex)的描述符为"([CII[CIII)I"。
5.4 attributes_count attribute_info
在描述符之后还有数量为attributes_count的attribute_info,attribute_info描述字段的额外信息,但这些额外信息最终存放在属性表中。如“final static int m = 123;”,那就可能会存在一项名称为ConstantValue的属性,其值指向常量123。
6 方法表集合
方法表和字段表结合几乎一样,理解了字段表,方法表就非常简单了。
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attribute_count |
由于volatile和transient不能修饰方法,所以方法表的访问标识中没有了ACC_VOLATILE,ACC_TRANSIENT标识。但同时又增加了代表synchronized native strictfp abstract的ACC_SYNCHRONIZED ACC_NATIVE ACC_STRICTFP ACC_ABSTRACT。
需要说明的是,方法表集合中并不包含方法里面的代码。方法代码经过编译后存放在方法属性集合中的一个名为"Code"的属性里面。例如某方法的属性表计数器attributes_count为1,则表示方法的属性表集合有一项属性,属性索引名称为0x0009,对应常量为code,说明此属性是方法的字节码描述。
7 属性表集合
Class文件、字段表、方法表都可以有自己的属性表,Java7里面定义了21种属性。
Code属性
并非所有方法表都有Code属性,比如接口和抽象类的方法就没有。结构如下:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | attribute_name_index | 1 | 属性名的索引,对Code属性而言恒为”Code” |
u4 | attribute_length | 1 | 属性值长度,相当于整个属性表长度长度减6(u2+u4) |
u2 | max_stack | 1 | 操作数栈深度最大值。JVM运行时根据此值分配栈桢的操作栈深度 |
u2 | max_locals | 1 | 局部变量表所需存储空间,单位是Slot,double和long占用2个Slot、其他基本类型1Slot,Slot空间可以重用(变量作用域问题) |
u4 | code_length | 1 | 编译后的字节码长度,理论上最长2^32-1,实际上JVM规定一个方法不允许超过65535条字节码指令 |
u1 | code | code_length | 代码编译后的字节码 |
u2 | exception_table_length | 1 | 异常表长度 |
exception_info | exception_table | exception_table_length | 异常表,记录字节码在start_pc到end_pc行之间如果出现类型为catch_type或其子类的异常则跳转到handler_pc行继续处理 |
u2 | attibutes_count | 1 | 属性表计数器 |
attribute_info | attributes | attibutes_count | 属性额外描述,比如描述变量初始化值在常量池中的索引 |
字节码值得注意的一个地方是,javac编译时将this关键字作为一个普通方法参数由JVM调用时自动传入。
Exceptions属性
描述方法可能抛出的受检异常。
LineNumberTable属性
描述Java远吗行号与字节码行号之间映射关系,也就是为什么抛异常的时候可以显示源码哪一行抛出的。
LocalVariableTable属性
描述栈桢中局部变量表与Java源码中变量的关系,以保证编译后的代码被其他代码调用时,IDE可以显示参数名(否则被arg0、arg1之类的变量名代替)
SourceFile属性
描述生成当前Class文件的源文件名称,也是抛异常时可以显示源文件名字的原因。但内部类不会生成这个属性。
ConstantValue属性
static关键字修饰的变量可以使用这个属性。对于Sun javac编译器,final static的变量采用ConstantValue属性初始化,其他static变量在(类构造器)中初始化。
InnerClasses属性
记录内部类和宿主类的关联。内部类和宿主类的Class文件都会有这个属性。
Signature属性
记录泛型签名信息。Java的泛型是使用擦除式实现的伪泛型,编译后擦除泛型,这个属性为了弥补此缺陷,方便反射API可以拿到泛型类型。