类文件结构及字节码指令
1. Class类文件的结构
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数、表。
无符号数属于基本的数据类型,以u1、u2、u4、u8分别表示1、2、4、8个字节的无符号数。无符号数可以用来描述数字、索引引用、数量值、按照UTF8编码构成的字符串值。
表是由多个无符号数、其他表作为数据项构成的复合数据类型。所有表都习惯性以“_info”结尾。
每个Class文件对应一个ClassFile结构。Class文件中字节序为Big-Endian。
ClassFile {
u4 magic; //Class 文件的标志
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
u2 access_flags;//Class 的访问标记
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口数量
u2 interfaces[interfaces_count];//一个类可以实现多个接口
u2 fields_count;//Class 文件的字段数量
field_info fields[fields_count];//一个类会可以有个字段
u2 methods_count;//Class 文件的方法数量
method_info methods[methods_count];//一个类可以有个多个方法
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
}
以下,按照在ClassFile中出现的顺序讲解。
1.1 魔数、Class文件的版本
魔数(majic)的作用是确定这个文件是否为一个能被虚拟机接受的Class文件。魔数为0xCAFEBABE。
u4 magic; //Class 文件的标志
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
第5、6个字节是次版本号(Minor Version)
第7、8个字节是主版本号(Major Version)
Java版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1。
JDK版本号:
- J2SE 8 = 52
- J2SE 7 = 51
- J2SE 6.0 = 50
- J2SE 5.0 = 49
- JDK 1.4 = 48
- JDK 1.3 = 47
- JDK 1.2 = 46
- JDK 1.1 = 45
1.2 常量池
constant_pool_count、constant_pool[constant_pool_count-1]
常量池可以理解为Class文件中的资源仓库。
常量池的容量计数是从1开始的。指向常量池的索引值需要表达“不引用任何一个常量池项目”时,将索引值设置为0。
常量池主要存放两大类常量:字面量(文本字符串、声明为 final 的常量值等)、符号引用(类和接口的全限定名、字段的名称和描述符、方法的名称和描述符)。
常量池中的所有项具有如下通用格式:
cp_info{
u1 tag;
u1 info[];
}
其中,tag具有下面值及其表示的含义:
类型 | 标志(tag) | 描述 |
---|---|---|
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_Utf8,其值采用UTF8缩略编码表示,\u0001—\u007F,用一个字节表示,\u0080—07FF,使用两个字节表示,\u0800—\uFFFF,用三个字节表示。
CONSTANT_Utf8_info结构:
CONSTANT_Utf8_info{
u1 tag;
u2 length;
u1 bytes[];
}
CONSTANT_Class_info结构:
CONSTANT_Class_info{
u1 tag;
u2 name_index;
}
1.3 访问标志
access_flags
1.4 类索引、父类索引、接口索引集合
this_class、super_class、interfaces_count、interfaces[interfaces_count]
Class文件中由这三项数据来确定这个类的继承关系。
类索引用于确定这个类的全限定名。
父类索引用于确定这个类的父类的全限定名。
接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句后的接口顺序从左到右排列在接口索引集合中。如果这个类本身是一个接口,则用extends可以继承多个接口,这些被继承的接口也放在这个接口索引集合中。
用this_class举例:
this_class指向常量池中一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info中的索引值(name_index)可以找到定义在CONSTANT_Utf8_info常量中的全限定名字符串。
1.5 字段表集合
fields_count、fields[fields_count]
字段表集合用来描述类、接口中定义的变量,包括类变量、实例变量,不包括在方法内部声明的局部变量。
字段表集合中不会列出从父类、父接口中继承而来的字段。但有可能列出原本Java代码中不存在的字段,如内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
fields_info结构如下:
-
access_flags
-
descriptor_index
表示字段的描述符。在下面1.6的方法表集合章节中,也会出现表示方法的描述符,下面一起讲解。
描述符的作用是描述字段的数据类型,方法的参数列表(数量、类型、顺序)、方法的返回值类型。
描述符规则是:
标识字符 含义 标识字符 含义 B 基本类型byte J 基本类型long C 基本类型char S 基本类型short D 基本类型double Z 基本类型boolean F 基本类型float V 特殊类型void I 基本类型int L 对象类型,如Ljava/lang/Object 对于数组,每一维度使用一个[描述,如String[][],用[[Ljava/lang/String;表示。
描述方法时,按照先参数列表,再返回值的顺序描述,参数列表严格按照参数顺序放在()中,如int index(char c)描述为(C)I。
-
attribute_info
用来描述字段的额外信息,如字段的初始值等。
1.6 方法表集合
methods_count、methods[methods_count]
method_info结构如下:
-
access_flags
-
descriptor_index
参见上面1.5字段表集合中的descriptor_index。
-
attribute_info
描述方法的额外信息,如方法体代码等。
方法体代码编译成字节码后,存放在该方法属性表集合(也就是该attribute_info)中的Code属性里。见1.7属性表集合中的Code属性。
如果子类没有重写父类的方法,方法表集合中不会出现来自父类的方法。但可能出现由编译器自动为类添加的方法,如类构造器
、实例构造器 。
1.7 属性表集合
attributes_count、attributes[attributes_count]
Class文件、字段表结合、方法表集合中都有属性表集合。
属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性。
attribute_info通用的结构:
attribute_name_index是引用常量池中的一个常量,属性的结构是完全自定义的,通过attribute_length说明长度,info给出属性值。
虚拟机预定义的属性:
-
Code
Code属性表的结构:
max_stack代表了栈帧中操作数栈的最大深度。
max_locals代表了局部变量表所需的存储空间,单位是Slot。
Slot是虚拟机为局部变量分配内存的最小单位:对于byte、char、float、int、short、boolean、returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot;对于double、long这两种64位的数据类型需要两个Slot存放。
局部变量表中的Slot可以重用,因此max_locals并不是所有局部变量所占Slot之和。
code_length、code[code_length]存储Java源代码编译后生成的字节码指令。
字节码指令,每个指令就是一个u1的单字节。u1范围是0—255,Java 虚拟机已经定义了200多条指令。
code_length理论上最大为2^32-1,但是虚拟机规范明确限制一个方法不允许超过65535条字节码指令。
Java在每段可能分支路径之后,都将finally语句块冗余一遍实现finally语义。
-
Exceptions
列举方法中可能抛出的受检异常(非RuntimeException),也就是方法描述时在throws后面列举的异常。
-
LineNumberTable
用于描述Java源码行号与字节码行号之间的对应关系。
javac使用-g:none、-g:lines取消、生成这项信息。
如果不生成LineNumberTable属性,对程序运行最主要的影响是抛出异常时,堆栈中不会显示出错的行号,在调试程序的时候,无法按照源码行设置断点。
-
LocalVaribleTable
用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。
javac使用-g:none、-g:vars取消、生成这项信息。
如果不生成LocalVaribleTable,最大的影响是别人引用这个方法时,所有的参数名称都会丢失,IDEA会使用arg0、arg1占位符代替。
-
LocalVaribleTypeTable
descriptor_index替换成Signature,用来描述泛型类型。
-
SourceFile
用来记录生成这个Class文件的源码文件名称。
javac使用-g:none、-g:source取消、生成这项信息。
如果不生成这项属性,当抛出异常时,堆栈中不会显示出错代码所属的文件名。
-
ConstantValue
通知虚拟机自动为静态变量赋值,只有static修饰的变量才可以使用这个属性。
类变量赋值的方式:类构造器
、ConstantValue属性。 Sun Javac编译器:如果同时使用final、static修饰一个变量,并且这个变量的数据类型是基本类型、java.lang.String,生成ConstantValue属性进行初始化;如果这个变量没有被final修饰,或者并非基本类型、java.lang.String,会选择
进行初始化。 -
InnerClasses
用于记录内部类与宿主类之间的关联。
-
Deprecated
用于类、字段、方法,表示被标注的部分不再推荐使用,在代码中使用@deprecated注解产生。
-
Synthetic
表示字段、方法不是由Java源码直接产生的,而是由编译器自行添加的。
所有由非用户代码产生的类、方法、字段都应当至少设置Synthetic属性或ACC_SYNTHETIC标志中的一项,唯一的例外是
、 。 -
StackMapTable
在虚拟机类加载的字节码验证阶段被新类型检查验证器使用。
-
Signature
用于记录泛型签名信息。
Java反射能获取到泛型类型,最终数据来源就是这个属性。
-
BootstrapMethods
用于保存invokedynamic指令引用的引导方法限定符。
2. 字节码指令
Java虚拟机指令 = 操作码(1个字节)+ 操作数(0个至多个)
Java虚拟机指令集中,大多数指令都包含了其操作所对应的数据类型信息。i代表int,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。
大部分指令没有支持byte、char、short、boolean类型。编译器会在编译期或运行期将byte、short数据带符号扩展为相应int,将boolean、char数据零扩展为相应int。
2.1 加载、存储指令
- 局部变量加载到操作数栈:load、load_
- 操作数栈存储到局部变量表:store、store_
- 常量加载到操作数栈:ipush、ldc、const、*const_
- 扩充局部变量表的访问索引:wide
2.2 运算指令
运算指令用于对操作数栈顶的两个值操作,再将结果重新存入到操作数栈顶。
add、sub、mul、div、rem(取余)、neg(取反)、shr、shl(左右移位)、or(按位或)、and(按位与)、xor(按位异或)、iinc(局部变量自增)、cmp(比较)
2.3 类型转换指令
i2c、i2b、i2s、l2i、f2i、f2l、d2i、d2l、d2f
2.4 对象创建、访问
- 创建类实例:new
- 创建数组:newarray、anewarray、multianewarray
- 访问类变量:getstatic、putstatic
- 访问实例变量:getfield、putfield
- 数组元素加载到操作数栈:*aload
- 操作数栈值存储到数组元素:*astore
- 取数组长度:arraylength
- 检查类实例类型:instanceof、checkcast
2.5 操作数栈管理指令
- 将操作数栈顶一个、两个元素出栈:pop、pop2
- 复制栈顶一个、两个数值,并将复制的值重新压入栈顶:dup_、dup2_
- 将栈顶两个数值互换:swap
2.6 控制转移指令
- 条件分支:if、if_icmp
- 复合条件分支:tableswitch、lookupswitch
- 无条件分支:goto、jsr、ret
2.7 方法调用指令
- invokevirtual:调用对象的实例方法
- invokeinterface:调用接口的方法
- invokespecial:调用特殊处理的实例方法,如实例初始化方法、私有方法、父类方法
- invokestatic:调用类方法
- invokedynamic:运行时动态解析出调用点限定符所引用的方法
- *return:方法返回指令
2.8 异常处理指令
- athrow:Java代码中的throw语句。
- 异常处理(catch语句)不是由字节码指令实现的,而是采用异常表实现的。
2.9 同步指令
- 同步方法、同步代码块都是由管程实现的。
- 同步方法不需要字节码指令控制,由方法表集合中的访问控制标志ACC_SYNCHRONIZED声明。
- 同步代码块由monitorenter、monitorexit指令保证。