Class文件(1): 类文件结构

Write Once, Run Anywhere.

各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(ByteCode),是构成平台无关性的基石。

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有任何分隔符。当遇到占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。

Class文件中只有两种数据类型:无符号数

  • 无符号数:属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
  • 表:是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表习惯性地以“_info”结尾。表用于描述有层次关系的复合数据结构,整个Class文件本质上就是一张表。

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
fields_info fields fields_count
u2 methods_count 1
methods_info methods methods_count
u2 attributes_count 1
attribute_info attributes attributes_count

由于Class文件没有分隔符等,所以上表所示的数据项,无论是顺序还是数量,甚至是数据存储的字节序(Byte Ordering),都是严格限定的,不允许改变。

魔数与Class文件的版本

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

很多文件存储标准中都使用魔数来进行身份识别,譬如gif或jpeg等在文件头中都存有魔数。

紧接着魔数存储的是Class文件的版本号,第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。

Java的版本号是从45开始的,JDK 1.1之后每个JDK大版本发布主版本号加1(JDK 1.0 ~ JDK 1.1 使用了45.0 ~ 45.3的版本号),JDK 1.8主版本号最大值为52.0。

常量池

紧接着主版本号之后的是常量池入口,可以把常量池当做Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一。

概要

Class文件中只有常量池容量计数值(constant_pool_count)是从1开始计数而不是0.

第0项常量空出来的目的:在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义。

常量池中主要存放两类常量:字面量(Literal)和符号引用(Symbolic References)。

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

Java代码在进行javac编译的时候不会有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。Class文件中不会保存各个方法、字段的最终内存布局信息,这些字段、方法的符号引用需要经过运行期转换才能得到直接引用(真正的内存入口地址)。

常量池的项目类型

常量池中的每一项常量都是一个表。每个表的结构都各不相同,共同的特点就是表开始的第一位是一个u1类型的标志位(tag),代表当前这个常量属于哪种类型的常量类型。

常量池在JDK 1.7之前有11种表,在JDK 1.7中为了更好地支持动态调用,又额外增加了3中(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info、CONSTANT_InvokeDynamic_info)。

常量池的项目类型

类型 标志 描述
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 表示一个动态方法调用点

访问标志

在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否声明为final等。

具体的标志位及其含义(一共有16个标志位可以使用,但是当前虚拟机规范中只定义了8个)

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0010 如果为类的话,是否被声明为final
ACC_SUPER 0x0020
ACC_INTERFACE 0x0200 标志这是一个接口
ACC_ABSTRACT 0x0400 标志这是一个抽象类
ACC_SYNTHETIC 0x1000 标志这个类并非由用户代码产生的
ACC_ANNOTATION 0x2000 标志这是一个注解
ACC_ENUM 0x4000 标志这是一个枚举类

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

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引结合(interfaces)是一组u2类型的数据集合,Class文件中用着三项数据确定这个类的继承关系。

  • 类索引:确定这个类的全限定名。
  • 父类索引:确定这个类的父类的全限定名。
  • 接口索引集合:描述这个类实现了哪些接口,按照implements顺序从左到右排列在集合中。

字段表集合

字段表集合(field_info):用于描述接口或类中声明的变量。字段(field)包括类变量和实例变量,不包括方法中的局部变量。

在Java语言中字段是无法重载的,不能使用一样的名称;但是对于字节码来说,如果两个字段的描述符不一样,那字段重名就是合法的。

字段包括的信息有:

  1. 字段的作用域(public、protected、private)
  2. 实例变量or类变量(static修饰符)
  3. 可变性(final修饰符)
  4. 并发可见性(volatile修饰符)
  5. 可否被序列化(transient修饰符)
  6. 字段数据类型(基本类型、对象、数组)
  7. 字段名称

上述信息中,各个修饰符都可以用布尔值来表示,因而字段表结构中也使用访问标志(access_flags)来统一处理。

字段表结构:

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

字段表结构中其中的name_index代表字段的“简单名称”,descriptor_index代表字段的“描述符”。
简单名称:指没有类型和参数修饰的方法或字段名称。如方法inc()和字段m的简单名称就是incm
描述符:作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。如一个定义为java.lang.String[][]类型的二位数组将被记录为[[Ljava/lang/String;,方法int indexOf(Char[] source, int fromIndex)的描述符为([CI)I

字段表最后的attribute_info属性表集合用于存储一些额外的信息,如定义一个常量final static int m = 123;那么可能就会存在一个名称为ConstantValue的属性,其值指向常量123。(后面会继续介绍属性表)

字段表不会列出从超类或父接口中继承而来的字段,但是可能会列出原本代码中不存在的字段(譬如内部类中指向外部类实例的字段)。

方法表集合

方法表的结构和字段表的结构描述几乎一样,只是在访问标志和属性表集合的可选项中有所区别。

在Java语言里,方法的特征签名是方法名以及参数类型和顺序,相同特征签名的方法不能共存;但是在Class文件中特征签名范围更大一些,只要描述符不完全一致就可以共存。

方法里的代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为Code的属性里面。

与字段表集合对应的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现父类的方法信息。但是,有可能出现由编译器自动添加的方法,如实例构造器<init>()方法和类构造器<clinit>()方法。

属性表集合

在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
《Java虚拟机规范(Java SE7)》版中,预定义属性增加到21项。

与Class文件中其他的数据项目要求严格的殊勋、长度和内容不同等,属性表集合的限制稍微宽松了些,不要求严格顺序,且只要不与已有属性名重复即可,Java虚拟机会在运行时忽略掉不认识的属性。

Code属性

Code属性出现在方法表集合中,但并非所有方法表都必须存在这个属性,例如接口中的方法就不存在Code属性。

Code属性表的结构

类型 名称 数量
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
  1. attribute_name_index:一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为“Code”。
  2. attribute_length:属性值得长度,也就是整个属性表除去前面6个字节的长度。
  3. max_stack:代表操作数栈(Operand Stacks)深度的最大值。占用两个字节,最大为65535。虚拟机运行时是根据这个值来分配栈帧(Stack Frame)中的操作数深度。(JVM没有寄存器,因而JVM的指令主要是从操作数栈而不是寄存器中取得操作数的,运行方式是基于栈而不是基于寄存器的)
  4. max_locals:代表了局部变量表所需的存储空间。max_locals的单位是SlotSlot是虚拟机为局部变量分配内存所使用的最小单位。局部变量表中的Slot可以重用,Javac编译器会根据变量的作用域来分配Slot的使用量,计算出max_locals
  5. code_lengthcode:用来存储Java源程序编译后生成的字节码指令,每个指令是一个u1类型的单字节(最多也就256条指令)。虚拟机限制了一个方法不允许超过65535条字节码指令,也就是code_length实际只用了u2的长度。

异常表及finally语义

编译器使用异常表而不是简单的跳转命令来实现Java异常以及finally机制

finally语义实现:JDK 1.4.2之后,实现finally语句改为编译器自动在每段可能的分支路径之后都讲finally的语句块冗余生成一遍来实现finally语义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ExceptionInfoTest {
static int x;
public static int inc(){
try {
x = 1; // 1. 测试正常执行
// x = 1/0; // 2. 测试try语句出现catch的异常
// x = 1/0; // 3. 将catch异常改为NullPointerException,测试try语句块中出现未被catch的异常
return x;
}catch (Exception e){
x = 2;
return x; // 4. 注释掉return语句,并throw new ArithmeticException(); 测试catch中出现异常
}finally {
x =3;
System.out.println("finally execute!");
}
}
public static void main(String[] args) {
System.out.println(inc());
}
}

编译器会为这段Java源代码生成3条异常表记录,对应3条可能出现的异常执行路径,分别为:

  • 如果try语句块中出现被catch的异常,则转到catch语句块处理。
  • 如果try语句块中出现未被catch的异常,则转到finally语句块处理。
  • 如果catch语句块中出现任何异常,则转到finally语句块处理。

finally是否一定会执行?
正常情况下,finally对应的try语句块得到执行时,finally语句块才会执行。但是也不排除在此之前使用System.exit(0);等手段终止虚拟机运行,这样就不会执行了。

Exceptions属性

Exceptions属性与Code属性是平级的,作用是列举出丰富中可能抛出的受查异常(Checked Exception),也就是丰富描述时throws关键字后面列举的异常。

上面介绍的异常表是编译器用于实现Java异常以及finally机制的。

LineNumberTable属性

LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系,非运行时属性(可以用于调试,抛出异常进行定位以及按照源码行号来设置断点)。

LocalVariableTable属性

LocalVariableTable属性用于描述栈帧中的局部变量表中的变量与Java源码中定义的变量之间的关系。非运行时属性(对于开发时,会造成方法引用时,所有参数名丢失,IDE使用诸如arg0/arg1等占位符替代)。

SourceFile属性

SourceFile属性用于记录生成这个Class文件的源码文件名称,非运行时属性
在Java中,对于大多数类来说,类名和文件名是一致的,但是有些特殊情况例如(如内部类)。如果不生成这属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。

ConstantValue属性

ConstantValue属性的作用是通知虚拟机自动为静态变量(static修饰的)赋值。

实例变量赋值:在实例构造器<init>()方法中。
类变量赋值:两种方式,在类构造器<clinit>()方法中或者使用ConstantValue属性。

目前Sun Javac编译器对类变量赋值的选择:

  • 如果是同时被finalstatic修饰的变量(常量),并且常量的数据结构类型是基本类型或者java.lang.String的话(也就是属性值是在常量池中),就生成ConstantValue属性来初始化。
  • 如果没有被final修饰或并非基本类型及字符串,则会选择在<clinit>()方法中进行初始化。

ConstantValue语义,虚拟机规范中并没有强制要求字段被设置为ACC_FINAL标志,只要求字段必须设置ACC_STATIC标志。对于final关键字的要求是在javac编译器里加入的。

InnerClass属性

InnerClass属性用于记录内部类与宿主类之间的关联。

Deprecated及Synthetic属性

Deprecated属性用于表示字段、方法或类已经不被推荐使用,可以使用注解@deprecated
Synthetic属性代表此字段或方法并不是由Java源码直接产生的,而是由编译器自行添加的。JDK 1.5之后可以使用ACC_SYNTHETIC标志位实现。

StackMapTable属性

StackMapTable属性是JDK 1.6增加的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载过程中的验证阶段被新类型验证器(Type Checker)使用。

目的在于替代以前比较消耗性能的基于数据流分析的类型推导验证器。

Signature属性

Signature属性在JDK 1.5增加的定长属性,可以出现于类、属性表和方法表结构的属性表中。
主要针对泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),Signature属性会记录泛型签名信息。

泛型的坏处就是运行期无法向C#等有真泛型支持的语言那样,将泛型类型与用户定义的普通类型同等对待,例如反射时无法获得泛型信息。而Signature属性就是为了弥补这个缺陷而增设的,现在Java的反射API能够获取泛型,最终数据来源就是这个属性。

BootstrapMethods属性

BootstrapMethods属性在JDK 1.7增加到Class文件规范中,是一个变长属性,位于类文件的属性表中。这个属性用于保存invokedynamic指令引用的引导方法限定符。