Class文件结构

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

虚拟机也是语言无关的,实现语言无关性的基础任然是虚拟机字节码存储格式。Java虚拟机不和包括Java在内的任何语言绑定,它只与Class文件这种特定的二进制文件格式所关联,Class文件中包含Java虚拟机指令集符号表以及若干其他辅助信息。

虚拟机的语言无关性

基于安全考虑Java虚拟机规范要求在Class文件中使用许多强制性的语法结构化约束,任何一门功能性语言都可以表示为一个能被Java虚拟机所接受的有校Class文件。

Java语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的,因此字节码命令所能提供的语义描述能力肯定会比Java语言本身更强大。

Class类文件的结构

任何一个Class文件都对应着唯一一个类或接口的定义信息,但类或接口并不一定都定义在文件中,也可以通过类加载器直接生成

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中无任何分割符,Class文件中存储的内容几乎全部是程序运行的必要数据,当遇到需要占用8位字节以上空间的数据项时,按照高位在前的方式分割成若干8位字节进程存储。

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 attribute_count 1
attribute_info attributes attributes_count

魔数与Class文件版本

每个Class文件的头4个字节magic称为魔数,它唯一的作用是确定这个文件是否是一个能被虚拟机接受的Class文件。很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式。Class文件的魔数值为0xCAFEBABE

紧接着的魔数的4个字节minor_versionmajor_version存储的是Class文件的次版本号主版本号。Java版本号是从45开始的,每个JDK大版本发布主版本号向上加1,高版本的JDK能向下兼容,但不能运行以后版本的Class文件,即使文件格式未发生变化,虚拟机也拒绝执行超过器版本号的Class文件。

1
2
3
4
5
6
public class com.coms.jvm.ClassFileConstantPool.ClassTest
SourceFile: "ClassTest.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:

静态常量池

紧接着主次版本号之后的constant_pool_countconstant_pool常量池入口,常量池可也理解为Class文件中的资源仓库,是Class文件结构中与其他项目关联最多的数据类型,也是Class文件中占用空间最大的数据项目之一,同时它还是在Class文件中第一个出现表类型数据的项目。

常量池中常量不是固定的,所以常量池的入口需要用一个u2类型的constant_pool_count来表示常量池容量计数值。容量计数是从1开始,目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达不引用任何常量池项目时可以把索引值设置为0。Class文件结构中只有常量池的容量计数时从1开始,其他集合类型接口索引集合、字段表集合、方法表集合等容量计数都是从0开始。

常量池中主要存放字面量符号引用两大类常量。字面量比较接近Java层面的常量,如文本字符串、声明为fianl的常量值等。符号引用属于编译原理方面的概念,包括类和接口的全限定名、字段和名称和描述符、方法的名称和描述符三类常量。

如果将ClassTest类放到com.jvm包下,则ClassTest类的全限定名com.jvm.ClassTest。JVM编译器将类编译成class文件后,此全限定名在class文件中是以二进制形式存储的,它会把全限定符.换成/,即在class文件中存储的ClassTest类的全限定名是com/louis/jvm/ClassTest

Java代码在进行Javac编译时,在Class文件中不会保存各个方法、字段内存布局信息,这些字段、方法的符号引用不经过运行期转换无法得到真正的内存入口地址,也无法直接被虚拟机使用,在虚拟机加载Class文件的时候进行动态连接。虚拟机运行时需要从常量池获得对应的符号引用,在类创建时或运行时解析、翻译到具体的内存地址中。

常量池中每一项常量都是一个,JDK1.7中共14种表,且这14种表各自均有自己的结构,这14种表有一个共同特点开始的第一位是一个u1类型标志位tag,代表当前常量属于哪种常量类型。下表是14种常量类型所代表的具体含义:

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

CONSTANT_Class_info型常量的结构如下表所示,tag是标志位用于区分常量类型name_index是一个索引值,它指向常量池中一个CONSTANT_Utf8_info类型常量,代表该类或接口的全局限定名

类型 名称 数量 
u1 tag 1
u2 name_index 1

CONSTANT_Utf8_info型常量的结构如下表所示,tag是标志位用于区分常量类型length值表示UTF-8编码的字符串长度的字节数,紧跟着的长度位length字节的连续数据是一个使用UTF-8缩略编码表示的字符串。

UTF-8缩略编码与普通编码的区别是从\u0001\u007f之间的字符的缩略编码使用一个字节表示,从\u0080\u00ff之间的字符的缩略编码使用两个字节表示,从\u0800\uffff之间的字符的缩略编码按照普通UTF-8编码规则使用三个字节表示。

Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度就是Java中方法、字段名的最大长度,这里的长度就是length的最大值,即u2类型能表达的最大值65535,故Java程序中变量、方法名的定义不能超过64KB英文字符,否则将无法被编译

类型 名称 数量 
u1 tag 1
u2 length 1
u1 bytes length
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Constant pool:
#1 = Methodref #3.#15 // java/lang/Object."<init>":()V
#2 = Class #16 // com/coms/jvm/ClassFileConstantPool/ClassTest
#3 = Class #17 // java/lang/Object
#4 = Utf8 date
#5 = Utf8 Ljava/util/Date;
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 Lcom/coms/jvm/ClassFileConstantPool/ClassTest;
#13 = Utf8 SourceFile
#14 = Utf8 ClassTest.java
#15 = NameAndType #6:#7 // "<init>":()V
#16 = Utf8 com/coms/jvm/ClassFileConstantPool/ClassTest
#17 = Utf8 java/lang/Object

其中<init>编译器添加的实例构造器

访问标志

紧接着常量池之后的u2类型的access_flags访问标志,用于识别一些类或接口层次的访问信息,包括该Class是还是接口,是否定义为public类型,是否定义位abstract类型,如果是类是否被修饰为final等。

标志名 标志值 标志含义 针对的对像
ACC_PUBLIC 0x0001 是否为public类型 所有类型
ACC_FINAL 0x0010 是否为final类型
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语义 类和接口
ACC_INTERFACE 0x0200 标识为接口类型 接口
ACC_ABSTRACT 0x0400 是否为抽象类型 类和接口
ACC_SYNTHETIC 0x1000 标识该类不由用户代码生成 所有类型
ACC_ANNOTATION 0x2000 标识为注解类型 注解
ACC_ENUM 0x4000 标识为枚举类型 枚举

access_flags中一共有16个标志位可以使用,当前只定义了其中8个,没有使用到的标志位要求一律为0。当JVM在编译某个类或者接口的源代码时,JVM会解析出这个类或者接口的访问标志信息,然后将这些标志设置到访问标志。

标志位取值
访问标识图解

1
2
3
4
5
6
public class com.coms.jvm.ClassFileConstantPool.ClassTest
SourceFile: "ClassTest.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:

索引

在Class文件中的索引包括类索引、父类索引、接口索引集合,Class文件中由这三项数据来确定该类的继承关系类索引父类索引都是u2类型的数据,接口索引集合一组u2类型的数据集合。

类索引用于确定该类全限定名父类索引用于确定该类的父类全限定名,接口索引集合用于描述该类实现了哪些接口。

类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引他们各自指向一个CONSTANT_Class_info型常量,通过CONSTANT_Class_info类型的常量中的name_index索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串

对于接口索引集合,入口的u2类型的数据为接口计数器interfaces_count,表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。

字段表集合

字段表用于描述接口或类中声明的变量,包括字段的作用域publicprivateprotected、是实例变量还是类变量static、可变性final、并发可见性volatile、可否被序列化transient、字段数据类型(基本数据类型、数组、对象)、字段名称,字段包括类变量、实例变量,但是不包括方法内部声明的局部变量

修饰符都是布尔值使用标志位来表示,放在access_flags项目中,字段名字被定义为什么数据类型引用常量池中的常量来描述,name_indexdescriptor_index都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。简单名称是指没有类型和参数的修饰的方法或字段名称描述符是用来描述字段的数据类型、方法的参数列表,包括数量、类型、顺序和返回值

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

ACC_PUBLICACC_PRIVATEACC_PROTECTED三个标志最多只能选择其一,ACC_FINALACC_VOLATILE不能同时选择,接口中必须有ACC_PUBLICACC_STATICACC_FINAL标志。

标志名称 标志值 含义
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_TRANSTENT 0x0080 字段是否为transient
ACC_SYNCHETIC 0x1000 字段是否为由编译器自动产生
ACC_ENUM 0x4000 字段是否为enum

基本数据类型byte、char、double、float、int、long、short、boolean以及代表无返回值的void类型都用一个大写字符来表示,对象类型用字符L加对象的全限定名来表示。

数组类型每个维度将使用一个前置的[字符来描述,如果定义一个java.lang.String[][]类型的二维数组,将被记录为[[Ljava.lang.String,整形的int[]将被记为[I。用描述符来描述方法时,按照先参数列表后放回值的顺序描述,参数列表按照参数的严格顺序放在一个小括号()内,如方法String desc(char[] a, int b,long[] c)的描述符为([CI[J)Ljava/lang/String;

标志符 含义
B 基本数据类型byte
C 基本数据类型char
D 基本数据类型double
F 基本数据类型float
I 基本数据类型int
J 基本数据类型long
S 基本数据类型short
Z 基本数据类型boolean
V 基本数据类型void
L 对象类型

字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出Java代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

Java语言字段无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但对字节码来说,两个字段的描述符不一致字段的重名是合法的。

方法表集合

Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方法,方法表的结构如同字段表一样,依次是访问标志、名称索引、描述符索引、属性表集合。这些数据项目含义非常类似,仅访问标志属性表集合可选项中所有区别。

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

方法里的Java代码经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为Code的属性里,属性表作为Class文件格式中最具扩展性的一种数据项目。

标志名称 标志值 含义
ACC_PUBLIC 0x0001 方法是否为public
ACC_PRIVATE 0x0002 方法是否为private
ACC_PROTECTED 0x0004 方法是否为protected
ACC_STATIC 0x0008 方法是否为static
ACC_FINAL 0x0010 方法是否为final
ACC_SYHCHRONRIZED 0x0020 方法是否为synchronized
ACC_BRIDGE 0x0040 方法是否是有编译器产生的方法
ACC_VARARGS 0x0080 方法是否接受参数
ACC_NATIVE 0x0100 方法是否为native
ACC_ABSTRACT 0x0400 方法是否为abstract
ACC_STRICTFP 0x0800 方法是否为strictfp
ACC_SYNTHETIC 0x1000 方法是否是有编译器自动产生的

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

在Java中要重载一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中字段符号引用的集合,由于返回值不包含在特征签名中,故Java中无法仅仅依靠返回值来重载方法。但在Class文件中特征签名范围更大,只要描述符不完全一致的两个方法也可以共存,两个方法有相同名称和特征值返回值不同,就可以合法共存一个Class文件中。

属性表集合