前言
本文是《深入理解Java虚拟机》第6章的部分知识点,这一章正如作者所说,对数据结构的讲解确实枯燥,对于失眠治疗真的是非常有效,本人经常看着看着就睡着了。因为内容比较多,所以本人对部分章节就浅尝辄止了(水一水)。本文的目标是能让读者对类文件结构与字节码指令有一个大概的了解,想要了解更多,读者可以查阅原文。
本章知识点
- 类文件的结构
- 字节码指令
类文件的结构
Class文件是一组以8位字节为基础单位的二进制流,各项数据严格按照顺序紧凑地排列且中间没有任何分隔符,当遇到数据项需要占用8位字节以上空间时,会按照高位在前的方式分割成若干个8位字节进行存储。
Class数据结构以一种伪结构来存储数据,该伪结构中只有两种数据类型:无符号数和表。无符号数指的是基本数据类型,以u1、u2、u4、u8来代表1个字节、2个字节、4个字节、八个字节的无符号数,它可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。表由多个无符号数或者其他表作为数据项构成的复合数据类型,他们的特征是都以_info结尾。
Class文件的格式如下表所示:
类型 | 英文名 | 中文名 | 数量 |
---|---|---|---|
u4 | magic | 魔数 | 1 |
u2 | minor_version | 次版本号 | 1 |
u2 | major_version | 主版本号 | 1 |
u2 | constant_pool_count | 常量计数器 | 1 |
cp_info | constant_pool | 常量池 | constant_pool_count-1 |
u2 | access_flags | 访问标志 | 1 |
u2 | this_class | 类索引 | 1 |
u2 | super_class | 父类索引 | 1 |
u2 | interfaces_count | 接口计数器 | 1 |
u2 | interfaces | 接口索引 | interfaces_count |
u2 | fields_count | 字段计数器 | 1 |
field_info | fields | 字段表 | fields_count |
u2 | methods_count | 方法计数器 | 1 |
method_info | methods | 方法表 | methods_count |
u2 | attributes_count | 属性计数器 | 1 |
attribute_info | attributes | 属性表 | attributes_count |
下面,我们一起来看看Class文件格式中各数据项的具体含义。
魔数
魔数的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。Class文件的魔数值为0xCAFEBABE,可以记作"咖啡宝贝"。
版本号
版本号用于判断Class文件的版本是否满足当前JDK版本。高版本的JDK可以向下兼容低版本的Class文件,但不能向上兼容超过其版本号的Class文件。
常量池
由于常量池中常量的数量是不固定的,所以需要使用常量计数器来标识常量的数量。该计数器是从1开始而不是0,目的是在于满足后面某些指向常量池的索引值在特定情况下需要表达“不使用任何一个常量池项目”的含义。
常量池中存放两类常量:字面量和符号引用。字面量包含了文本字符串、声明为final的常量值等。符号引用包含了类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
访问标志
访问标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否是public类型;是否是abstract类型;如果是类,是否被声明为final等。
类索引、父类索引与接口索引集合
类索引确定这个类的全限定名,父类索引确定这个类的父类的全限定名,因为Java不允许多继承,所有父类索引只有一个(只有java.lang.Object的父类索引为0)。接口索引集合用于描述这个类实现了哪些接口,这些被实现的接口在集合中按照源码内implements后的顺序从左到右排序。
字段表集合
字段表用于描述接口或者类中声明的变量(不包括方法内部的局部变量)。它包含了字段的修饰符信息,各修饰符都使用布尔值来代表是否存在,至于字段叫什么名字以及被定义为什么数据类型,因为长度无法固定,所以引用常量池中的常量来描述。
方法表集合
方法表用于描述类中的方法,它包括了方法的修饰符信息,各修饰符都使用布尔值来代表是否存在,至于方法叫什么名字以及返回什么数据类型,因为长度无法固定,所以引用常量池中的常量来描述。至于方法中的代码,会经过编译器编译成字节码指令后,存放到方法属性表集合中一个名为Code的属性中。
属性表集合
属性表集合存在Class文件、字段表、方法表中,用于描述某些场景专有的信息。与Class文件其他数据项不同,属性表集合不再要求各个属性表具有严格顺序,只要不与已有属性重复即可。属性表集合预定义了许多种属性名称,如:Code、ConstantValue、Exceptions等。在这里,便不对这些属性名称进行展开了,读者可以翻看《深入理解Java虚拟机》了解各属性的含义。
字节码指令
字节码指令由操作码(1个字节长度、有特定含义的数字)和操作数(0个或多个所需参数)构成。
字节码与数据类型
对于大多数字节码指令,它们的操作码助记符中都有特殊的字符来表明其为哪种数据类型服务,如下表所示:
类型 | 字符 |
---|---|
int | i |
long | l |
short | s |
byte | b |
char | c |
float | f |
double | d |
reference | a |
也有一些操作码助记符没有代表数据类型的特殊字符,如arraylength指令,goto指令等。需要注意的一点是,大多数对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的int类型作为运算类型。
加载和存储指令
加载和和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。它包含如下几类指令:
- 将一个局部变量加载到操作栈:iload, iload_<n>, etc.
- 将一个数值从操作数栈存储到局部变量表:istore, istore_<n>, etc.
- 将一个常量加载到操作数栈:bipush, sipush, ldc, etc.
- 扩充局部变量表的访问索引:wide
运算指令
运算指令用于对两个操作数栈上的值进行运算,并把结果存入到操作栈顶。它包含如下几类指令:
- 加法指令:iad, ladd, fadd, dadd
- 减法指令:isub, lsub, fsub, dsub
- 乘法指令:imul, lmul, fmul, duml
- 除法指令:idiv, ldiv, fdiv, ddiv
- 求余指令:irem, lrem, frem, drem
- 取反指令:ineg, lneg, fneg, dneg
- 位移指令:ishl, ishr, iushr, lshl, lshr, lushr
- 按位或指令:ior, lor
- 按位与指令:iand, land
- 按位异或指令:ixor, lxor
- 局部变量自增指令:iinc
- 比较指令:dcmpg, dcmpl, fcmpg, fcmpl, lcmp
类型转换指令
类型转换指令可以将两种不同的数值类型进行相互转换,JVM直接支持小范围类型向大范围类型的安全转换,相对的,处理大范围类型到小范围类型的窄化类型转换,则需要显示地使用转换指令来完成,这些指令包括:i2b, i2c, i2s, l2i, f2i, f2l, d2i, d2l和d2f,需要注意的是,窄化类型转换会导致结果产生不同的正负号、不同的数量级、数值精度丢失的情况。
对象创建与访问指令
类实例与数组都属于对象,但是其创建与操作使用了不同的字节码指令,指令如下:
- 创建类实例:new
- 创建数组:newarray, anewarray, multianewarray
- 访问类字段(static字段)和实例字段:getfield, putfield, getstatic, putstatic
- 把一个数组元素加载到操作数栈:baload, caload, saload, iaload, laload, faload, etc.
- 将一个操作数栈的值存储到数组元素中:bastore, castore, sastore, iastore, etc.
- 取数组长度:arraylength
- 检查类实例类型:instanceof, checkcast
操作数栈管理指令
操作数栈管理指令用于直接操作操作数栈,指令如下:
- 将操作数栈的栈顶1个或2个元素出栈:pop, pop2
- 复制栈顶1个或2个数值重新压入栈顶:dup, dup2, dup_x1, dup2_x1, etc.
- 将栈最顶端的两个数值互换:swap
控制转移指令
控制转移指令可以让JVM有条件或无条件地执行指定位置指令,指令如下:
- 条件分支:ifeq, iflt, ifle, ifne, ifgt, ifge, etc.
- 复合条件分支:tableswitch, lookupswitch
- 无条件分支:goto, goto_w, jsr, jsr_w, ret
方法调用和返回指令
方法调用指令有如下:
- invokevirtual:用于调用对象的实例方法
- invokeinterface:用于调用接口方法
- invokespecial:用于调用一些特殊处理的实例方法,包括实例初始化方法,私有方法和父类方法
- invokestatic:用于调用类方法
- invokedynamic:用于在运行时动态解析出调用点限定符所引用的方法(非固化在JVM内部的方法)
返回指令是根据返回值的类型区分的,包括return(声明void方法), ireturn, lreturn, freturn, dreturn, areturn.
异常处理指令
在Java程序中显式抛出异常的操作都由athrow指令来实现,至于处理异常,则不是由字节码指令来实现。
同步指令
JVM指令集中有monitorenter(开始同步)和monitorexit(退出同步)两条指令来支持synchronized关键字的语义。
总结
类文件由一组8位字节为基础单位的二进制流按特定的数据项顺序组成,中间没有分隔符。其中各数据项由无符号数和表组成。数据项的顺序为:魔数(咖啡宝贝)、版本号(主+次)、常量池(字面量+符号引用)、访问标志(标识类或接口)、类索引(类的全限定名)、父类索引(父类的全限定名)、接口索引集合(接口集合的全限定名)、字段表集合(字段信息)、方法表集合(方法信息)、属性表集合(类文件、字段表、方法表中的专有信息)。
字节码指令由操作码(1个字节长度、有特定含义的数字)和操作数(0个或多个所需参数)构成。指令根据类型可以分为:加载和存储指令(数据在局部变量表和操作数栈之间传输)、运算指令(操作数栈上的值运算)、类型转换指令(安全转换与窄化转换)、对象创建与访问指令(数组和对象)、操作数栈管理指令(压栈出栈)、控制转移指令(跳转)、方法调用和返回指令、异常处理指令、同步指令(开始/退出同步)。
...
// hey guy!
if( isValuable(this.article) && (like(this.article) || follow("ccoke"))) {
System.out.println("Thank you! XD");
} else {
System.out.println("I will continue to work hard!T.T");
}
...