Java字节码是Java虚拟机所使用的指令集。
在解释执行过程中,每当为Java方法分配栈桢时,Java虚拟机往往需要开辟一块额外的空间作为操作数栈,来存放计算的操作数以及返回结果。
具体来说便是:执行每一条指令之前,Java虚拟机要求该指令的操作数已被压入操作数栈中。在执行指令时,Java虚拟机会将该指令所需的操作数弹出,并且将指令的结果重新压入栈中。下面我们来看个例子:
public class HelloWorld {
public int firstStack() {
int a = 100;
int b = 100;
int c = 100;
return (a+b)*c;
}
}
以加法指令iadd为例。假设在执行该指令前,栈顶的两个元素分别为int值和1和int值2,那么iadd指令将弹出这两个int,并将求得的和int值3压入栈中。
Java字节码中有好几条指令是直接作用在操作数栈上
的。常见的有:
dup
:复制栈顶元素。常用于复制new指令所生产的未经初始化的引用。pop
:舍弃栈顶元素。常用于舍弃调用指令的返回结果。- swap:交换栈顶两个元素的值。
需要注意的是,上述两条指令只能处理非long
或者非double
类型的值,这是因为long类型或者double类型的值,需要占据两个栈单元。当遇到这些值时,我们需要同时复制栈顶两个单元的dup2
指令,以及弹出栈顶两个单元的pop2
指令。
//这段代码的foo方法中,当执行new指令时,Java虚拟机将指向一块已分配的、
//未初始化的内存的引用压入操作数栈中。
public void foo() {
Object o = new Object();
}
// 对应的字节码如下:
public void foo();
0 new java.lang.Object [3]
3 dup
4 invokespecial java.lang.Object() [8]
7 astore_1 [o]
8 return
接下来,我们需要以这个引用为调用者,调用其构造函数,也就是上面字节码中的invokespecial
指令。要注意,该指令将消耗操作数栈上的元素
,作为它的调用者以及参数(不过Object的构造器不需要参数)。
因此,我们需要利用dup指令复制一份new指令的结果,并且来调用构造器。当调用返回之后,操作数栈上仍有原本由new指令生成的引用,可用于接下来的操作(即偏移量为7的字节码,下面会介绍到)。
在下面这段代码的foo方法中,我将调用静态方法bar,但是却不用其返回值。
由于对应的invokestatic
指令仍旧会将返回值压入foo方法的操作数栈中
,因此Java虚拟机需要额外执行pop指令,将返回值舍弃。
public static boolean bar() {
return false;
}
public void foo() {
bar();
}
// foo 方法对应的字节码如下:
public void foo();
0 invokestatic FooTest.bar() : boolean [24]
3 pop
4 return
在Java字节码中,有一部分指令可以直接将常量加载到操作数栈上
。以int类型为例,Java虚拟机即可以通过iconst
指令加载-1至5之间的int值,也可以通过bipush、sipush
加载一个字节、两个字节所能代表的int值。
Java虚拟机还可以通过ldc加载常量池中的常量值
,例如ldc#18将加载常量池中的第18项。这些常量包括int类型、long类型、float类型、double类型、String类型以及Class类型的常量。
局部变量表
Java方法栈桢的另一个重要组成部分则是局部变量表,字节码程序可以将计算的结果缓存在局部变量表之中
。实际上,Java虚拟机将局部变量表当成一个数组,依次存放this指针(仅非静态方法
),所传入的参数,以及字节码中的局部变量。
和操作数栈一样,long类型以及double类型的值将占据两个单元,其余类型仅占据一个单元。
下面来看个例子:
public void foo(long l, float f) {
{
int i = 0;
}
{
String s = "Hello, World";
}
}
以上面这段代码中的foo方法为例,由于它是一个实例方法,因此局部变量数组的第0个单元
存放着this指针
。
第一个参数为long类型,于是数组的1、2两个单元存放着所传入的long类型参数的值。第二个参数是float类型,于是数组的第3个单元存放着所传入的float类型参数的值,如下图所示:
在方法体里的两个代码块中,分别定义了两个局部变量i
和s
。由于这两个局部变量的生命周期没有重合之处,因此,Java编译器可以将它们编排至同一单元中。也就是说,局部变量数组的第4个单元将为i或者s。
存储在局部变量区的值,通常需要加载至操作数栈中,方能进行计算,得到计算结果后在存储至局部变量数组中。这些加载、存储指令是区分类型的。例如,int类型的加载指令为iload,存储指令为istore。
局部变量区访问指令表
局部变量数组的加载、存储指令都需要指明所加载单元的下标。举例来说,aload 0 指的是加载第0个单元所存储的引用,在前面示例中的foo方法里指的便是加载this指针。
在我印象中,Java字节码中唯一能够直接作用于局部变量区的指令是iinc M N(就不需要加载到操作数栈)
(M为非负整数,N为整数)。该指令指的是将局部变量数组的第M个单元中的int值增加N
,常用于for循环中自增量的更新。
public static int bar(int i) {
return ((i + 1) - 2) * 3 / 4;
}
// 对应的字节码如下:
Code:
stack=2, locals=1, args_size=1
0: iload_0 //把局部变量表中第一个int变量载入操作栈
1: iconst_1 //把常量1载入操作栈
2: iadd//将操作栈顶部的两个int型数据相加后出栈,并将结果入栈
3: iconst_2//将常量2载入操作栈
4: isub//将操作栈顶部另个int型数相减后出栈,并将结果入栈
5: iconst_3//将常量3载入操作栈
6: imul//将操作栈顶部另个int型数相乘后出栈,并将结果入栈
7: iconst_4//将常量4载入操作栈
8: idiv//将操作栈顶部另个int型数相除后出栈,并将结果入栈
9: ireturn 将操作栈顶部的int数值出栈返回
高层语义的字节码
Java相关指令,包括各类具备高层语义的字节码,即new(后跟目标类,生成该类的未初始化的对象),instanceof(后跟目标类,判断栈顶元素是否为目标类/接口的示例。是则压入1,否则压入0),checkcast(后台跟目标类,判断栈顶元素是否为目标类/接口的实例。如果不是便抛出异常),athrow(将栈顶异常抛出),以及monitorenter(为栈顶对象加锁)和monitorexit(为栈顶对象解锁)。
此外,该类型的指令还包括字段访问指令
,即静态字段访问指令
getstatic、putstatic,和实例字段访问指令
getfield、putfield。这四条指令均附带用以定位目标字段的信息
,但所消耗的操作数栈元素皆不同。
以putfield
为例,在下图中,它会把值v存储至对象obj的目标字段之中。
方法参数
方法的局部变量表,索引值从0开始,且小于局部变量表的长度。
对于实例方法,JVM会隐式传递一个指向当前实例的引用(this),作为方法的第0个局部变量
。因此,应用程序实际传递的参数是从索引值1开始的。
但是,对于类方法,由于不需要传递实例引用。因此,应用程序实际传递的参数是从索引值0开始的。
方法调用指令
方法调用指令,包括invokedstatic,invokedspecial,invokedvirtual,invokedinterface以及invokedynamic。除invokeddynamic外,其他的方法调用指令所消耗的操作数栈元素是根据调用类型以及目标方法描述符来确定的。在进行方法调用之前,程序需要一次压入调用者(invokestatic不需要),以及各个参数。
public int neg(int i) {
return -i;
}
public int foo(int i) {
return neg(neg(i));
}
// foo 方法对应的字节码如下:foo 方法对应的字节码如下:
public int foo(int i);
0 aload_0 [this]
1 aload_0 [this]
2 iload_1 [i]
3 invokevirtual FooTest.neg(int) : int [25]
6 invokevirtual FooTest.neg(int) : int [25]
9 ireturn
以上面这段代码为例,当调用foo(2)时,每条指令执行前后局部变量表
以及操作数栈
的分布如下所示:
invokevirtual
该指令用于调用对象的实例方法
,根据对象的实际类型进行分派(虚方法分派)。
指令带有一个表示索引的参数,运行时常量池在该索引处的项为某个方法的符号引用(提供类名称、方法名称及方法描述符信息)。
invokestatic
该指令用于调用类方法(static方法)
。
invokespecial
该指令用于调用一些需要特殊处理的实例方法
,包括实例初始化方法
、私有方法
和父类方法
。
返回指令表
控制流指令包括
- 无条件跳转goto
- 条件跳转指令
其中返回指令是区分类型的。例如,返回int值的指令为ireturn,如下图所示:
除返回指令外,其他的控制流指令均附带一个或者多个字节码偏移量,代表需要跳转到的位置。例如下面的abs方法中偏移量为1的条件跳转指令,当栈顶元素小于0时,跳转至偏移量为6的字节码。
public int abs(int i) {
if (i >= 0) {
return i;
}
return -i;
}
// 对应的字节码如下所示:
public int abs(int i);
0 iload_1 [i] // 变量i入操作数栈
1 iflt 6 // 若栈顶int类型值小于0则跳转, 调到第6行代码
4 iload_1 [i]
5 ireturn
6 iload_1 [i]
7 ineg
8 ireturn
详细指令参考
https://blog.youkuaiyun.com/github_35983163/article/details/52945845