0%

Java字节码

操作数栈

Java字节码是JVM所使用的指令集。它与JVM基于栈的计算模型是密不可分的。

解释执行过程中,每当为Java方法分配栈帧时,JVM往往需要开辟一块额外的空间作为操作数栈,来存放计算的操作数以及返回结果。

在JVM中,只有解释器完整的模拟出该计算模型。即时编译器在解析字节码时会使用一个虚拟的栈计算模型,但是在接下来的编译优化,以及生成的机器码就不用了。

执行每一条指令之前,JVM要求该指令的操作数已被压入操作数栈中。在执行指令时,JVM会将该指令所需的操作数弹出,并且将指令的结果重新压入栈中。

以加法指令iadd为例。iadd指令只消耗栈顶的两个元素,因此,对于离栈顶距离为2的元素,iadd指令并不关心它是否存在,更加不会对其进行修改。

Java字节码中有好几条指令时直接作用在操作数栈上的。最为常见的便是dup:复制栈顶元素;以及pop:舍弃栈顶元素。

dup与pop指令只能处理非long或者非double类型的值,long类型或者double类型的值,需要占据两个栈单元。当遇到这些值时,需要同时复制栈顶两个单元的dup2指令,以及弹出栈顶两个单元的pop2指令。

dup指令

dup指令常用语复制new指令所生成的未经初始化的引用。当执行new指令时,JVM将指向一块已分配的、未初始化的内存的引用压入操作数栈中。

接着,需要以这个引用为调用者,调用其构造器(invokespecial指令),该指令将消耗操作数栈上的元素,作为它的调用者以及参数(Object的构造器不需要参数)。

需要利用dup指令复制一份new指令的结果,并用来调用构造器。当调用返回之后,操作数栈上仍有原本由new指令生成的引用,可用于接下来的操作。

pop指令

pop指令常用于舍弃调用指令的返回结果。例如调用静态方法,但不用其返回值。

对应的invokestatic指令仍旧会将返回值压入调用方法的操作数栈中,因此JVM需要额外执行pop指令,将返回值舍弃。

swap指令

直接作用于操作数栈顶的指令,它将交换栈顶两个元素的值。

加载常量指令

直接将常量加载到操作数栈上。以int类型为例,JVM既可以通过iconst指令加载-1至5之间的int值,也可以通过bipush、sipush加载一个字节、两个字节所能代表的int值。

通过ldc加载常量池中的常量值。如ldc#18将加载常量池中的第18项。

这些常量包括int类型、long类型、float类型、double类型、String类型以及Class类型的常量。

正常情况下,操作数栈的压入弹出都是一条条指令完成的。唯一的例外情况是在抛异常时,JVM会清除操作数栈上的所有内容,然后将异常实例压入操作数栈上。

局部变量区

Java方法栈帧的另外一个重要组成部分则是局部变量区,字节码程序可以将计算的结果缓存在局部变量区之中。

JVM将局部变量区当成一个数组,依次存放this指针(仅非静态方法),所传入的参数,以及字节码中的局部变量。

局部变量必须初始化。

和操作数栈一样,long类型以及double类型的值将占据两个单元,其余类型仅占据一个单元。

存储在局部变量区的值,通常需要加载至操作数栈中,方能进行计算,得到计算结果后再存储至局部变量数组中。这些加载、存储指令时区分类型的。如,int类型的加载指令为iload,存储指令为istore。

局部变量数组的加载、存储指令都需要指明所加载单元的下标。例,aload 0指的是加载第0个单元所存储的引用(this指针)。

Java字节码中唯一能够直接作用于局部变量区的指令时iinc M N(M为非负整数,N为整数)。该指令指的是将局部变量数组的第M个单元中的int值增加N,常用于for循环中的自增量的更新。

综合示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//bar方法,接收一个int类型的参数,进行一系列计算之后再返回
public static int bar(int i) {
return ((i + 1) - 2) * 3 / 4;
}
// 对应的字节码如下:
Code:
stack=2, locals=1, args_size=1
0: iload_0
1: iconst_1
2: iadd
3: iconst_2
4: isub
5: iconst_3
6: imul
7: iconst_4
8: idiv
9: ireturn

对应的字节码中的stack=2,locals=1代表该方法需要的操作数栈空间为2,局部变量数组空间为1。

Java字节码简介

其他类别的指令。

Java相关指令,包括各类具备高层语义的字节码,new(后跟目标类,生成该类的未初始化的对象),instanceof(后跟目标类,判断栈顶元素是否为目标类/接口的实例。是则压入1,否则压入0),checkcast(后跟目标类,判断栈顶元素是否为目标类/接口的实例。如果不是便抛出异常),athrow(将栈顶异常抛出),monitorenter(为栈顶对象加锁)和monitorexit(为栈顶对象解锁)。
该类型的指令还包括字段访问指令:静态字段访问指令getstatic、putstatic,和实例字段访问指令getfield、putfield。这四条指令均附带用以定位目标字段的信息,但所消耗的操作数栈元素皆不同。

方法调用指令,包括invokestatic,invokespecial,invokevirtual,invokeinterface以及invokedynamic。除invokedynamic外,其他的方法调用指令所消耗的操作数栈元素是根据调用类型以及目标方法描述符来确定的。在方法调用之前,程序需要依次压入调用者(invokestatic不需要),以及各个参数。

数组相关指令,包括新建基本类型数组的newarray,新建引用类型数组的anewarray,生成多维数组的multianewarray,以及求数组长度的arraylength。另外,还包括数组的加载指令以及存储指令。这些指令时区分类型的,如int数组的加载指令为iaload,存储指令为iastore。

控制流指令,包括无条件跳转指令goto,条件跳转指令tableswitch和Lookupswitch(前者针对密集的cases,后者针对稀疏的cases),返回指令,以及被废弃的jsr、ret指令。其中返回指令时区分类型的。如返回int值的指令为ireturn(正常执行路径return,异常执行路径athrow)。
除返回指令外,其他的控制流指令均附带一个或者多个字节码偏移量,代表需要跳转到的位置。

其他,剩余的Java字节码几乎都和计算相关。

总结

Java方法的栈帧分为局部变量区和操作数栈。通常来说,程序需要将变量从局部变量区加载至操作数栈中,进行一番运算之后再存储回局部变量区中。

Java字节码可以划分为很多种类型,如加载常量指令,操作数栈专用指令,局部变量区访问指令,Java相关指令,方法调用指令,数组相关指令,控制流指令,以及计算相关指令。