实战java虚拟机(四)——Class文件结构
前言
对于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生态圈的基础和核心