下面的分析以如下的class文件为例:
CAFEBABE 00000034 00130A00 04000F09 00030010 07001107 00120100 016D0100 01490100 063C696E 69743E01 00032829 56010004 436F6465 01000F4C 696E654E 756D6265 72546162 6C650100 03696E63 01000328 29490100 0A536F75 72636546 696C6501 000E5465 7374436C 6173732E 6A617661 0C000700 080C0005 00060100 19636F6D 2F796F75 79652F63 6C617A7A 2F546573 74436C61 73730100 106A6176 612F6C61 6E672F4F 626A6563 74002100 03000400 00000100 02000500 06000000 02000100 07000800 01000900 00001D00 01000100 0000052A B70001B1 00000001 000A0000 00060001 00000003 0001000B 000C0001 00090000 001F0002 00010000 00072AB4 00020460 AC000000 01000A00 00000600 01000000 07000100 0D000000 02000E
Class文件结构
Class文件是一组以8字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符若遇到需要占用8字节以上空间的数据时,则会按照高位在前的方式分割成若干组8位字节进行存储。
Class文件格式采用一种类似于C语言结构体的伪结构来存储,这种结构只有两种数据类型:无符号数和表。
- 无符号数属于基本的数据类型,以u1,u2,u4,u8来分别代表1个字节,2个字节,4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值、或者按照UTF-8编码构成字符串值
- 表是由多个无符号数或其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾,整个Class文件本质上就是一张表。
ClassFile {
u4 magic; CAFEBABE
u2 minor_version; 0000
u2 major_version; 0034
u2 constant_pool_count; 0013
cp_info constant_pool[constant_pool_count-1]; 0A00****6A656374
u2 access_flags; 0021
u2 this_class; 0003
u2 super_class; 0004
u2 interfaces_count; 0000
u2 interfaces[interfaces_count]; 无
u2 fields_count; 0001
field_info fields[fields_count]; 0002****0000
u2 methods_count; 0002
method_info methods[methods_count]; 0001****0009
u2 attributes_count;
attribute_info attributes[attributes_count];
}
魔数
魔数代表文件的格式,由于文件的后缀名可以随意更改,所以在文件的开头用CAFFBABY代表Class文件,这种定义广泛用于各种文件定义中。例如png的魔数为0x89504E470D0A1A0A,jpeg的魔数为0xFFD8FF
版本号
主版本从JDK1.0到JDK1.8以十进制表示为45-52。版本号指明生成该class文件的JDK版本,在上表中知道生成该class文件的主版本为0x0034及十进制的52,所以生成该class文件的JDK主版本为1.8。说明这个class文件是可以被1.8或以上版本虚拟机执行。
常量池
常量池中的常量数量是不固定的,所以在常量池的开始用一个u2长度的数据代表常量的个数。常量池的下标从1开始,0用于后面某些常量池的索引值得数据在特定情况下需要表达"不引用任何一个常量池项目"的含义。常量池中主要存放两大类常量:字面量和符号引用
- 字面量:比较接近于Java语言层面的常量概念。比如字符串、fianl常量值
- 符号引用: 属于编译原理方面的概念,其包含下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
实例中代表常量池常量个数的数据为0x0013,即十进制的19,所有常量池中常量的个数为 19 - 1 =18.
常量池的项目类型
常量池中每一项常量都是一个表,总共14种,这14种表都有一个共同的特点,就是表的开始第一位是一个u1类型标志位
实例中的常量项分析如下所示:
#1 = Methodref
tag u1 标志位 10 0A
index u2 指向声明方法的类描述符CONSTANT_Class_info的索引项 第4个常量项 0004
index u2 指向名称及类型描述符CONSTANT_NameAndType的索引项 第15个常量项 000F
#2 = Fieldref
tag u1 标志位 9 09
index u2 指向声明字典的类或接口描述符CONSTANT_Class_info的索引项 第3个常量项 0003
index u2 指向字段描述符CONSTANT_NameAndType的索引项 第10个常量项 0010
#3 = Class
tag u1 标志位 7 07
index u2 指向全限定名常量项的索引 第17个常量项 0011
#4 = Class
tag u1 标志位 7 07
index u2 指向全限定名常量项的索引 第18个常量项 0012
#5 = Utf8
tag u1 标志位 1 01
length u2 UTF-8编码的字符串占用的字节数 1个字节 0001
bytes u1 长度为length的UTF-8编码的字符串 长度为1的UTF-8编码的字符串 > m 6D
#6 = Utf8
tag u1 标志位 1 01
length u2 UTF-8编码的字符串占用的字节数 1个字节 0001
bytes u1 长度为length的UTF-8编码的字符串 长度为1的UTF-8编码的字符串 > I 49
#7 = Utf8
tag u1 标志位 1 01
length u2 UTF-8编码的字符串占用的字节数 6个字节 0006
bytes u1 长度为length的UTF-8编码的字符串 长度为6的UTF-8编码的字符串 > <init> 3C696E69743E
#8 = Utf8
tag u1 标志位 1 01
length u2 UTF-8编码的字符串占用的字节数 3个字节 0003
bytes u1 长度为length的UTF-8编码的字符串 长度为3的UTF-8编码的字符串 > ()V 282956
#9 = Utf8
tag u1 标志位 1 01
length u2 UTF-8编码的字符串占用的字节数 4个字节 0004
bytes u1 长度为length的UTF-8编码的字符串 长度为4的UTF-8编码的字符串 > Code 436F6465
#10 = Utf8
tag u1 标志位 1 01
length u2 UTF-8编码的字符串占用的字节数 15个字节 000F
bytes u1 长度为length的UTF-8编码的字符串 长度为15的UTF-8编码的字符串 > LineNumberTable 4C696E65 4E756D62 65725461 626C65
#11 = Utf8
tag u1 标志位 1 01
length u2 UTF-8编码的字符串占用的字节数 3个字节 0003
bytes u1 长度为length的UTF-8编码的字符串 长度为3的UTF-8编码的字符串 > inc 696E63
#12 = Utf8
tag u1 标志位 1 01
length u2 UTF-8编码的字符串占用的字节数 3个字节 0003
bytes u1 长度为length的UTF-8编码的字符串 长度为3的UTF-8编码的字符串 > ()I 282949
#13 = Utf8
tag u1 标志位 1 01
length u2 UTF-8编码的字符串占用的字节数 10个字节 000A
bytes u1 长度为length的UTF-8编码的字符串 长度为10的UTF-8编码的字符串 > SourceFile 536F7572 63654669 6C65
#14 = Utf8
tag u1 标志位 1 01
length u2 UTF-8编码的字符串占用的字节数 14个字节 000E
bytes u1 长度为length的UTF-8编码的字符串 长度为14的UTF-8编码的字符串 > TestClass.java 54657374 436C6173 732E6A61 7661
#15 = NameAndType
tag u1 标志位 12 0C
index u2 指向该字段或方法名称常量项的索引 第7个常量项索引 0007 // <init>
index u2 指向该字段或方法描述符常量项的索引 第8个常量项索引 0008 // ()V
#16 = NameAndType
tag u1 标志位 12 0C
index u2 指向该字段或方法名称常量项的索引 第5个常量项索引 0005 // m
index u2 指向该字段或方法描述符常量项的索引 第6个常量项索引 0006 // I
#17 = Utf8
tag u1 标志位 1 01
length u2 UTF-8编码的字符串占用的字节数 25个字节 0019
bytes u1 长度为length的UTF-8编码的字符串 长度为25的UTF-8编码的字符串 > com/youye/clazz/TestClass 636F6D2F 796F7579 652F636C 617A7A2F 54657374 436C6173 73
#18 = Utf8
tag u1 标志位 1 01
length u2 UTF-8编码的字符串占用的字节数 16个字节 0010
bytes u1 长度为length的UTF-8编码的字符串 长度为16的UTF-8编码的字符串 > java/lang/Object 6A617661 2F6C616E 672F4F62 6A656374
访问标识符
访问标志用来标识类或接口的访问信息,包括Class是类还是接口,是否定义为public;是否定义为abstract;如果是类的话是否被申明为final等。具体的标志位以及含义如下所示
标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0001 是否被声明为final,只有类可设置
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语义,invokespecial指令的语义在JDK1.0.2发生过改变,为了区别这条指令使用哪种语义,JDK1.0.2之后编译出来的类的这个标志都必须为真。
ACC_INTERFACE 0x0020 标识这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类值为假
ACC_SYNTHETIC 0x1000 标志这个类并非由用户代码产生的
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x0400 标识这是一个枚举
在前面已经知道这个类是在JDK1.8.0下编辑的,所以ACC_SUPER 必须为真。则有 0x0021 ^ 0x0020 = 0x001, 即该类是由public修饰的。
再比如 该类的访问标识符为 0x0031,则有 0x0031 ^ 0020 = 0x0011。查表可以知道 0x0001 | 0x0010 = 0x0011。及标识该类是由 public final修饰的。
类索引
确定这个类的全限定名,它指向一个类型为CONSTANT_Class_info的类描述符常量,根据CONSTANT_Class_info类型中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串
父类索引
确定这个类的父类全限定名。它同样指向一个类型为CONSTANT_Class_info的类描述符常量。在Java中除了java.lang.Object无父类外,其他的类都有父类,所以父类索引都不为0。
接口计数器和接口索引集合
接口计数器标识该Class中含有多少接口,紧跟着接口计数器的是接口索引集合。接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身就是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。
而接口计数器表示接口索引表的容量。如果这个类没有实现接口,则计数器值为0,后面接口的接口索引表不再占用任何字节。
字段数量
字段包括类级别变量和对象级别变量,不包括方法内部定义的局部变量。
字段结构表
紧接着字段数量的是一个字段表结构,字段的作用域(private、protected、public),是实例变量还是对象变量(static修饰符),可变性(final修饰符),并发可见性(volatile修饰符)等等都在字段表接口中体现。
其中字段修饰符access_flags,和类中的access_flags类似,对于字段来说可以设置的标志位及含义如下:
标志名称 标志值 含义
ACC_PUBLIC Ox0001 字段是否是public
ACC_PRIVATE 0x0020 字段是否是private
ACC_PROTECTED 0x0004 字段是否是protected
ACC_STATIC 0x0008 字段是否是static
ACC_FINAL 0x0010 字段是否是final
ACC_VOLATILE 0x0040 字段是否是volatile
ACC_TRANSIENT 0x0008 字段是否是transient
ACC_SYNTHETIC 0x1000 字段是否是由编译器自动产生的
ACC_ENUM 0x4000 字段是否是enum
参数类型使用一个大写字母来表示如下表所示:
标识字符 含义 标识字符 含义
B byte J long
C char S short
D double Z boolean
F float V void
I int L 对象类型,如Ljava/lang/Object
对于数组类型,每个一维数组将使用一个前置的’[‘字符来描述。比如定义一个java.lang.String[][]类型的二维数组,将记录为’[[Ljava/lang/String’,一个double数组 double[]记为’[D’。
filed_info {
access_flags u2 1 0x0002 > private
name_index u2 1 0x0005 > m
descriptor_index u2 1 0x0006 > I
attributes_count u2 1 0x0000 > 属性表集合中无属性
attributes attribute_info attributes_count
}
根据上表,则可以推出定义这个字段的源代码为 private int m;
方法数量
与字段表集合相同的是,如果父类中的方法没有在子类中被重写,是不会出现在方法表中。但有可能会出现编译器自动添加的方法。
方法表结构
class文件存储格式中对方法的描述和对字段的描述几乎相同,方法表的结构也和字段表相同。不过方法表的访问标志和字段的不同,列出如下:
标识名称 标志值 含义
ACC_PUBLIC 0x0001 方法是否是public
ACC_PRIVATE 0x0002 方法是否是private
ACC_PUBLICPROTECTED 0x0004 方法是否是protected
ACC_STATIC 0x0008 方法是否是static
ACC_FINAL 0x0010 方法是否是final
ACC_SYNCHRONIZED 0x0020 方法是否是synchronized
ACC_BRIDGE 0x0040 方法是否是由编译器产生的桥接方法
ACC_VARARGS 0x0080 方法是否接受不定参数
ACC_NATIVE 0x0100 方法是否是native
ACC_ABSTRACT 0x0400 方法是否是abstract
ACC_STRICTFP 0x0800 方法是否是strictfp
ACC_SYNTHETIC 0x1000 方法是否是由编译器自动产生的
method_info {
access_flags u2 1 0x0001 > public
name_index u2 1 0x0007 > <init>
descriptor_index u2 1 0x0008 > ()V
attributes_count u2 1 0x0001 > 属性表集合有一项属性
attributes attribute_info attributes_count 0x0009 > 属性表集合中的一个属性名称索引为第9个常量项,即Code,说明此属性是方法的字节码描述
}
表中的Code属性在后面在分析
method_info {
access_flags u2 1 0x0001 > public
name_index u2 1 0x000B > inc
descriptor_index u2 1 0x000C > ()I
attributes_count u2 1 0x0001 > 属性表集合有一项属性
attributes attribute_info attributes_count 0x0009 > 属性表集合中的一个属性名称索引为第9个常量项,即Code,说明此属性是方法的字节码描述
}
Java类都要有一个构造方法,如果没有的话编译器会自动构造一个无参的构造方法,就是上面的第一个名叫的方法;同时,如果一个类中含有静态代码块或者静态变量,那么就需要首先执行类的构造方法,来执行静态代码块和初始化静态变量,这就是上面的第三个名为的方法。
不过,方法比字段还多了方法体呢,那方法体中的代码哪去了?
在每一个方法表中descriptor_index后描述属性的时候,0001表明属性的个数为1,再后面的000E是指向常量池中的CONSTANT_Utf8_info常量,内容是Code,说明后面属性中存放的就是方法体里面编译后的字节码指令。
在Java中,要重载一个方法,除了要与原方法具有相同的方法名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是特征签名只包含参数个数和类型,并不包含返回值类型,所以Java语言中是无法仅仅依靠返回值的不同来对一个方法重载的。但是在class文件格式中,特征签名还包括返回值类型,也就是说只有返回值类型不同的两个方法也可以存在。这一点在泛型中编译后类型擦除后生成的桥接方法上有所体现。不过这里就不过多介绍了。
属性数量和属性集合表
最常用的属性恐怕就是Code属性了,因为大多数的方法都会有编译后的字节码指令,这些指令就存储在方法表中的Code属性中。如果一个Java程序的信息可以分为代码(方法体中的代码)和元数据(包括类、字段、方法定义以及其它信息),那么Code属性存储的就是代码,其它所有的结构存储的都是元数据。不过并非所有的方法表都有这个Code属性,比如接口或抽象类中的方法表就不存在Code属性(JDK 1.8中的接口也可以定义方法了)
上面两个方法中的两个属性分别为
Code {
attribute_name_index u2 0009
attribute_length u4 0000001F
max_stack u2 0001
max_locals u2 0001
code_length u4 00000005
code x*u1 2AB70001 B1
exception_table_length u2 0000
exception_table exception_info 无
attributes_count u2 0001
attribute_info attributes 000A00 00000600 01000000 03
}
LineNumberTable {
attribute_name_index u2 000A
attribute_length u4 00000006
line_number_table_lenght u2 0001
line_number_table line_number_info line_number_table_length 0000 0003
}
Code {
attribute_name_index u2 0009
attribute_length u4 0000001F
max_stack u2 0002
max_locals u2 0001
code_length u4 00000007
code x*u1 2AB40002 0460AC
exception_table_length u2 0000
exception_table exception_info 无
attributes_count u2 0001
attribute_info attributes 000A00 00000600 01000000 03
}
LineNumberTable {
attribute_name_index u2 000A
attribute_length u4 00000006
line_number_table_lenght u2 0001
line_number_table line_number_info line_number_table_length 0000 0007
}
line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合,line_number_info表包括了start_cp 和 line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号
SourceFile属性记录生成这个class文件的源码文件名称。在上面的数据中,0001表示属性表集合中有一个属性,000D(即十进制13)是属性名的索引值,查找常量池可以知道是SourceFile,00000002是这个属性的长度,即两个字节,最后的两个字节就是这个属性的内容,是一个常量池索引,000E,十进制14,结果是Test.java。
至此Test.class以分析完毕