Class文件格式
Java设计之初,就考虑的是跨平台性和跨语言性,目前有越来越多的其他语言都可以直接需要在Java虚拟机,虚拟机只能识别Class文件,至于是由何种语言编译而来的,虚拟机并不关心。具体就是这个样子的:
class文件的整体概述
class文件是一组以8位字节为基本单位的二进制流。当需要占用8位以上的数据时,会按照Big-endian顺序,高位在前,低位在后的方式来分割成多个8位字节来存储。
Class文件采用类C结构体的伪结构来存储的,只有2中数据类型:
- 无符号数
- 是基本数据类型,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节的无符号数。可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
- 表
- 由多个无符号数获取其他表作为数据项构成的复合数据类型,习惯以“_info”结尾
一个Class类文件是由一个ClassFile结构组成:
ClassFile {
u4 magic; //魔数,固定值0xCAFEBABE
u2 minor_version; //次版本号
u2 major_version; //主版本号
u2 constant_pool_count; //常量的个数
cp_info constant_pool[constant_pool_count-1]; //具体的常量池内容
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]; //具体的属性内容
}
ClassFile文件详细分析
魔数
class文件标识位,用于确定这个Class文件是否能被虚拟机所接受。4个字节,固定值0xCAFEBABE
版本号
次版本号 + 主版本号
常量池
- 常量池长度:用u2类型代表常量池容量计数值
- 常量池内容
cp_info { u1 tag; u1 info[]; }
tag
字面量,与Java语言层面的常量概念相近,包含文本字符串、声明为final的常量值等。
常量池中每一项常量都是一个表结构,每个表的开始第一位是u1类型的标志位tag, 代表当前这个常量的类型。
每个常量类型都有自己的结构,具体请百度。
info[]
符号引用,包括:类和接口的全限定名、字段的名称和描述符和方法的名称和描述符
实例
Constant pool:
#1 = Methodref #10.#27 // java/lang/Object."<init>":()V
#2 = Fieldref #6.#28 // com/gamm/mobile/Test.a:I
#3 = Class #29 // java/lang/StringBuilder
#4 = Methodref #3.#27 // java/lang/StringBuilder."<init>":()V
#5 = Methodref #3.#30 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#6 = Class #31 // com/gamm/mobile/Test
#7 = String #32 // aa
#8 = Methodref #3.#33 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#9 = Methodref #3.#34 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#10 = Class #35 // java/lang/Object
#11 = Utf8 a
#12 = Utf8 I
#13 = Utf8 s
#14 = Utf8 Ljava/lang/String;
访问标识
2个字节代表,标示用于识别一些类或者接口层次的访问信息。
类索引、父类索引与接口索引
- 类索引:u2类型的数据,用于确定类的全限定名
- 父类索引:u2类型的数据,用于确定父类的全限定名
- 接口索引计算器:u2类型的数据,用于表示索引集合的容量
- 接口索引集合:一组u2类型的数据的集合,用于确定实现的接口(对于接口来说就是extend的接口)
字段表
字段表用于描述类或接口中声明的变量,格式如下:
field_info {
u2 access_flags; //访问标识
u2 name_index; //名称索引
u2 descriptor_index; //描述符索引
u2 attributes_count; //属性个数
attribute_info attributes[attributes_count]; //属性表的具体内容
}
用于描述接口或者类中声明的变量,包括类级变量和实例级变量,但不包括方法内部声明的局部变量;它不会列出从父类和超类继承而来的字段。
access_flags
name_index
表示该属性的名称在常量池的索引
descriptor_index
表示该属性的描述符在常量池的索引
attributes_count 和 attribute_info
具体的属性
方法表
method_info {
u2 access_flags; //访问标识
u2 name_index; //名称索引
u2 descriptor_index; //描述符索引
u2 attributes_count; //属性个数
attribute_info attributes[attributes_count]; //属性表的具体内容
}
访问标识:
- 对于方法里的Java代码,进过编译器编译成字节码指令后,存放在方法属性表集合中“code”的属性内。
- 当子类没有覆写父类方法,则方法集合中不会出现父类的方法信息。
实例
public void does();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: new #3 // class java/lang/StringBuilder
3: dup
4: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
7: aload_0
8: getfield #2 // Field a:I
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
14: ldc #7 // String aa
16: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_1
23: return
LineNumberTable:
line 12: 0
line 13: 23
LocalVariableTable:
Start Length Slot Name Signature
0 24 0 this Lcom/gamm/mobile/Test;
23 1 1 b Ljava/lang/String;
属性表
attribute_info {
u2 attribute_name_index; //属性名索引
u4 attribute_length; //属性长度
u1 info[attribute_length]; //属性的具体内容
}
属性表的限制相对宽松,不需要各个属性表有严格的顺序,只有不与已有的属性名重复,任何自定义的编译器都可以向属性表中写入自定义的属性信息,Java虚拟机运行时会忽略掉无法识别的属性。
Java虚拟机规范里预定义的属性:
Code属性
Java程序方法体中的代码经过javac编译后,字节码指令存放在Code属性,其属性表结构如下:
Exceptions属性
方法描述时throws关键字后面列举的异常,和Code属性里的异常表不同。
LineNumberTable属性
用于描述Java源码行号与字节码行号之间的对应关系,它不是必须的,可以通过javac -g:none取消该信息。没有该信息的影响是运行时抛异常不会显示出错的行号,在代码调试时无法按照源码行来设置断点。
LocalVariableTable属性
用于描述栈帧中局部变量与Java源码中定义的变量之间的关系,它不是运行时必须的,可以通过javac -g:none取消该信息。如果没有这个属性,所有的参数名称都会丢失,取之以arg0、arg1这样的占位符来替代。
SourceFile属性
用于记录生成这个Class的源码文件名称,这个属性也是可选的。
ConstantValue属性
作用是通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量才可以用这个属性。对于非static类型的变量的赋值是在实例构造器方法中进行的;而对于类变量有两种方式:在类构造器方法中或者使用ConstantValue属性。目前Sun javac编译器的选择是:同时使用final和static修饰的变量且为基本数据类型或String类型使用ConstantValue属性初始化,否则使用实例初始化。
InnerClass属性
用于记录内部类与宿主类之间的关联。
其中number_of_class代表需要记录多少个内部类信息,每个内部类的信息都由一个inner_class_info表进行描述。
Deprecated及Synthetic属性
Deprecated(不推荐使用)和Synthetic(不是由Java源码直接产生编译器自行添加的,有两个例外是实例构造器和类构造器)这两个属性都属于布尔属性,只存在有和没有的区别,没有属性值的概念。在属性结构中attribute_length的数据值必须为0x00000000。
Signature属性
一个可选的定长属性,在JDK 1.5发布后增加的,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量或参数化类型,则Signature属性会为它记录泛型签名信息。这主要是因为Java的泛型采用的是擦除法实现的伪泛型,在字节码中泛型信息编译之后统统被擦除,在运行期无法将泛型类型与用户定义的普通类型同等对待。通过Signature属性,Java的反射API能够获取泛型类型。
实例
Signature: #21 // <T:Ljava/lang/Object;>Ljava/lang/Object;
SourceFile: "Test.java"
InnerClasses:
#6= #5 of #3; //Inner=class com/gamm/mobile/Test$Inner of class com/gamm/mobile/Test
补充
- 通过xxd或hexdump可以查看class文件的二进制信息
- 通过javap可以查看实际的内容,javap -verbose XXX
- 虚拟机实现的方式主要有两种:将输入的Java虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集或宿主主机CPU的本地指令集。
全限定名
是指把类全名中的“.”号,用“/”号替换,并且在最后加入一个“;”分号后生成的名称。比如java.lang.Object对应的全限定名为java/lang/Object;
字段描述符:更简单的用于描述字段的数据类型
方法描述符:用来描述方法的参数列表(数量、类型以及顺序)和返回值
格式:(参数描述符列表)返回值描述符。 例如:Object m(int i, double d, Thread t) {..} ==> IDLjava/lang/Thread;)Ljava/lang/Object;