前言
对于Java虚拟机来说,Class文件是虚拟机的一个重要接口。无论使用何种语言开发,只要能将源文件编译成正确的Class文件,那么这种语言就可以在Java虚拟机上运行。
Class文件总体结构如下图所示
在Java虚拟机规范中,Class文件使用一种类似于C语言结构体的方式进行描述,并且统一使用无符号整数作为基本数据类型,由u1,u2,u4,u8分别表示无符号单字节,2字节,4字节和8字节整数,对于字符串,使用u1数组进行表示。
因此,一个Class文件可以非常严谨地被描述成:
ClassFile{
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
Class文件的结构严格按照该结构体的定义:(以二进制格式打开Class文件分析)
(1) 文件以一个4字节的Magic(魔数)开头,紧跟着大、小版本号。
(2) 在版本号之后是常量池,常量池的个数为constant_pool_count,常量池中的表项有constant_pool_count -1项。
(3) 常量池之后是类的访问修饰符、代表自身类的引用、父类引用及接口数量和实现的接口引用。
(4) 在接口之后,有字段的数量和字段描述、方法数量及方法的描述。
(5) 存放类文件的属性信息
一、魔数
魔数作为Class文件的标志,用来告诉Java虚拟机,这是一个Class文件。它是4字节的无符号整数,固定为0xCAFEBABE。
当一个文件不以0xCAFEBABE开头,则虚拟机进行文件校验时就会抛出以下错误:
Exception in thread “main” java.lang.ClassFormatError: Incompatible magic value 184466110 in class file ***
二、 版本
魔数后面紧跟的是小版本号与大版本号,首先是小版本号,然后是大版本号,它们都是2字节的无符号整数
Class文件的版本号与Java编译器的对应关系如下表
大版本号
小版本号
编译器版本
46
0
1.2
47
0
1.3
48
0
1.4
49
0
1.5
50
0
1.6
51
0
1.7
52
0
1.8
53
0
1.9
54
0
10
这是从我项目中随机取的一个class文件,通过观察第一行魔数后面的4个字节可知,0x34 = 52十进制,因此是1.8版本编译的
三、常量池
常量池是Class文件内容最丰富的区域之一,它对于Class文件中字段和方法的解析也有至关重要的作用,版本号之后紧跟的是常量池的数量,以及若干个常量池表项
常量池表项的类型及其TAG值如下表
常量池类型
TAG
CONSTANT_Class
7
CONSTANT_Methodref
10
CONSTANT_String
8
CONSTANT_Float
4
CONSTANT_Double
6
CONSTANT_Utf8
1
CONSTANT_MethodType
16
CONSTANT_Fieldref
9
CONSTANT_InterfaceMethodref
11
CONSTANT_Integer
3
CONSTANT_Long
5
CONSTANT_NameAndType
12
CONSTANT_MethodHandle
15
CONSTANT_InvokeDynamic
18
继续使用上一个class的内容,第一行8、9列可以看出,0x15 = 21,该文件中的常量池表项有21-1=20项(常量池0位空缺项,不存放实际内容)。数量之后就是常量池实际内容。
四、Class的访问标记
常量池后紧跟的是访问标记,用2字节表示,用于表明类的访问信息,如public,final,abstract等
类Access Flag的标记位和含义
标记名称
数值
描述
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
枚举
(表中数值为了容易描述而取的特殊值)
比如,0x0021表示该类为public且ACC_SUPER标记置为1
写一个简单的类
编译后部分class文件如下
0x0421表明该类是抽象类,且为public,ACC_SUPER置为1(一般都会为1,继承object类)
五、当前类、父类和接口
在访问标记后,会指定该类的类别、父类类别和实现的接口,格式如下:
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
其中this_class和super_class都是2字节无符号整数,指向常量池一个CONSTANT_Class,表示当前类型和父类,由于可以继承多个接口,因此,需要用数组形式保存多个接口的索引,表示接口的每个索引也是一个指向常量池的CONSTANT_Class
继续使用该class,其中00 02,00 03表明的是本类和父类在常量池的索引,后面的00 00表明继承接口数量为0,因此interfaces数组没有数据
六、Class文件的字段
接口描述后,会有类的字段信息
u2 fields_count
field_info fields[fields_count]
字段数量fields_count是一个2字节无符号整数,field_info是字段具体信息,结构如下:
field_info{
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
字段Access Flag的标记位及含义
标记名称
取值
描述
ACC_PUBLIC
0x0001
表示public字段
ACC_PRIVATE
0x0002
表示private字段
ACC_PROTECTED
0x0004
表示protected字段
ACC_STATIC
0x0008
表示静态字段
ACC_FINAL
0x0010
表示final字段
ACC_VOLATILE
0x0040
表示volatile字段
ACC_TRANSIENT
0x0080
表示瞬时字段,在持久化读写时忽略该字段
ACC_SYNTHETIC
0x1000
由编译器产生的方法,没有源码对应
ACC_ENUM
0x4000
枚举
继续使用上面的例子,从offset的00000272第10列开始,00 01表明的是字段数量,该类中数量为1,后续00 19表明这个字段是public static final字段,接下来00 04为常量池索引,表示字段名称,常量池第四个为a,即字段名称为a,00 05为字段类型描述,常量池第5个java/lang/String,表示为String类型,接着是属性数量,00 01表示字段存在一个属性, 后面的00 06为属性名,常量池第6项为ConstantValue,表示该属性为常量属性。
常量属性的结构为
ConstantValue_attribute{
u2 attribute_name_index;
u4 attribute_length;
u2 constantvalue_index;
}
之后连续4字节为属性剩余长度,00 00 00 02,表示从0x00000002之后的2字节为属性全部内容,00 07即常量池第7项,得到常量CONSTANT_String,值为空。所以分析出该常量字段为public static final String a =“”;
七、class文件的方法基本结构
在字段之后,就是类的方法信息,它由两部分组成
u2 methods_count;
method_info methods[methods_count];
每个method_info表示一个方法,结构如下
method_info{
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
方法访问标记取值
标记名称
值
作用
ACC_PUBLIC
0x0001
public
ACC_PRIVATE
0x0002
private
ACC_PROTECTED
0x0004
protected
ACC_STATIC
0x0008
静态方法
ACC_FINAL
0x0010
final方法不可被重载重写
ACC_SYNCHRONIZED
0x0020
同步方法
ACC_BRIDGE
0x0040
由编译器产生的桥接方法
ACC_VARARGS
0x0080
可变参数方法
ACC_NATIVE
0x0100
native
ACC_ABSTRACT
0x0400
抽象方法
ACC_STRICT
0x0800
浮点模式为FP-strict
ACC_SYNTHETIC
0x1000
编译器产生的方法,没有源码对应
访问标记name_index表示方法名称,是指向常量池的索引
descriptor_index为方法描述符,也是指向常量池的索引,表示方法的签名(参数、返回值等)
后面的attributes_count和attribute_info表示属性数量及描述
attribute_info{
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
常用属性
属性
作用
ConstantValue
字段常量
Code
表示方法的字节码
StackMapTable
Code属性的描述属性,用于字节码变量类型验证
Exceptions
方法的异常信息
SourceFile
类文件的属性,表示生成这个类的源码
LineNumberTable
Code属性的描述属性,描述行号和字节码的对应关系
LocalVariableTable
Code属性的描述属性,描述函数局部变量表
BootstrapMethods
类文件的描述属性,存放类的引导方法,用于invokeDynamic
StackMapTable
Code属性的描述属性,用于字节码类型校验
Code属性较为复杂,整理一下作用与结构成下图
其中对于Class还有InnerClasses属性和Deprecated属性,比较少用到这里就不多说。
八、Class文件总结
Class文件是非常庞大的,这里我只摘抄一些比较重要的点做一些笔记,后续随着Java平台的发展,Class文件也会有很多补充,但是基本的结构和文件格式应该是不会做重大调整的。从Java虚拟机的角度看,通过Class文件可以让更多的计算机语言支持Java虚拟机平台,Class文件不仅仅是Java虚拟机的执行入口,更是Java生态圈的基础和核心