对象的创建与访问指令
Java是面向对象的程序设计语言,虚拟机平台从字节码层面就对面向对象做了深层次的支持。有一系列指令专门用于对象操作,可进一步细分为创建指令、字段访问指令、数组操作指令、类型检查指令。
创建指令
虽然类实例和数组都是对象,但是Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令:
-
创建类实例的指令
-
创建类实例的指令:new
它接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后,将对象的引用压入栈。
-
创建类实例的指令:new
-
创建数组的指令:
-
创建数组的指令: newarray、anewarray、multianewarray。
- newarray: 创建基本类型数组
- anewarray:创建引用类型数组
-
multianewarray:创建多维数组
上述创建指令可以用于创建对象或者数组,由于对象和数组在Java中的广泛使用,这些指令的使用频率非常高。
-
创建数组的指令: newarray、anewarray、multianewarray。
public void newInstance() {
Object obj = new Object();
File file = new File("Hello World");
}
字节码指令
0 new #2 <java/lang/Object> 在堆中开辟空间,并在栈的栈帧的操作数栈里创建引用,指向堆中的对象
3 dup 将操作数栈中的引用,复制一份
4 invokespecial #1 <java/lang/Object.<init> : ()V> 将操作数栈中的元素出栈,即复制的那一份,出栈了
7 astore_1 将出栈的元素,存入在局部变量表中
8 new #3 <java/io/File> 在堆中开辟空间,在操作数栈中入栈一个引用数据,指向堆中开辟的空间,即堆的地址
11 dup 复制一份引用,并入操作数栈
12 ldc #4 <Hello World> 从常量池中加载字符串到操作数栈,即字符串的地址
14 invokespecial #5 <java/io/File.<init> : (Ljava/lang/String;)V> 将操作数栈顶端的两个元素出栈,即字符串引用和对象引用出栈
17 astore_2 将对象引用存入局部变量表中
18 return
public void newArray() {
int[] intArray = new int[10];
Object[] objects = new Object[10];
int[][] minArray = new int[10][10];
String[][] strArray = new String[10][];
}
字节码指令:
0 bipush 10
2 newarray 10 (int)
4 astore_1
5 bipush 10
7 anewarray #2 <java/lang/Object>
10 astore_2
11 bipush 10
13 bipush 10
15 multianewarray #6 <[[I> dim 2
19 astore_3
20 bipush 10
22 anewarray #7 <[Ljava/lang/String;>
25 astore 4
27 return
字段访问指令
对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素。
-
访问类字段(static字段,或者称为类变量)的指令:
getstatic、putstatic
-
访问类实例字段(非static字段,或者称为实例变量)的指令:
getfield、putfield
举例:
以getstatic指令为例,它含有一个操作数,为指向常量池的Fieldref索引,它的作用就是获取Fieldref指定的对象或者值,并将其压入操作数栈。
数组操作指令
数组操作指令主要又:xastore和xaload指令。具体为:
- 把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
- 将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、lastore、fastore、dastore、aastore
数组类型 | 加载指令 | 存储指令 |
---|---|---|
byte(boolean) | baload | bastore |
char | caload | castore |
short | saload | sastore |
int | iaload | iastore |
long | laload | lastore |
float | faload | fastore |
double | daload | dastore |
reference | aaload | aastore |
-
取数组长度的指令:arraylength
该指令弹出栈顶的数组元素,获取数组的长度,将长度压入栈。 -
指令xaload表示将数组的元素压栈,比如saload,caload分别表示压入short数组和char数组。指令xaload在执行时,要求操作数中栈顶元素为数组索引i,栈顶顺位第2个元素为数组引用a,该指令会弹出栈顶这两个元素,并将a[i]重新压入堆栈。
-
xastore则专门针对数组操作,以iastore为例,它用于给一个int数组的给定索引赋值。在iastore执行前,操作数栈顶需要以此准备3个元素:值、索引、数组引用,iastore会弹出这3个值,并将值赋给数组中指定索引的 位置。
public void setArray() {
int[] intArray = new int[10];
intArray[3] = 20;
System.out.println(intArray);
}
0 bipush 10 # 操作数栈中放10这个值
2 newarray 10 (int) # 创建一个具体的数组,其实操作数栈中存放的是引用,执行堆空间的一块区域,这里10已经用过了,就会出操作数栈
4 astore_1 # 将引用保存在局部变量表中,索引为1的位置,并且操作数栈的引用出栈了
5 aload_1 # 将局部变量表中的引用地址,再入栈操作数栈中
6 iconst_3 # 操作数栈中加载一个常量3
7 bipush 20 # 操作数栈中加载一个常量20
9 iastore # 保存:需要将操作数栈顶的三个元素(值,索引,数组引用)出栈,即完成数组中指定元素赋值操作
10 getstatic #8 <java/lang/System.out : Ljava/io/PrintStream;> #将指定元素#8放入操作数栈
13 aload_1 # 将局部变量表中,索引为1的位置的元素放入操作数栈中
14 iconst_1 # 将常量1放入操作数栈中
15 iaload # 将栈顶的两个元素出栈(数组索引和数组引用),并重新将a[i]重新压入栈中
16 invokevirtual #9 <java/io/PrintStream.println : (I)V>
19 return
类型检查指令
检查类实例或数组类型的指令:instanceof、checkcast
- 指令checkcast用于检查类型强制转换是否可以进行。如果可以进行,那么checkcast指令不会改变操作数栈,否则它会抛出ClassCastException异常。
- 指令instancof用来判断给定对象是否是某一个类的实例,它会将判断结果压入操作数栈。
public String checkCast(Object obj) {
if (obj instanceof String) {
return (String)obj;
} else {
return null;
}
}
0 aload_1 # 将局部变量表中索引为1的位置的元素,放入操作数栈
1 instanceof #10 <java/lang/String> # 判断操作数栈顶元素,是否是#10,即String,
4 ifeq 12 (+8) # 如果判断成立,继续执行如下操作;不成立,跳转到第12行的指令
7 aload_1 # 将局部变量表中索引为1的位置的元素,加入操作数栈中
8 checkcast #10 <java/lang/String> # 检查是否可以转换,如果可以,则转换。但是不会改变操作数栈
11 areturn # 引用类型返回
12 aconst_null # 不能进行转换,则null入栈
13 areturn # 引用类型返回
方法调用与返回指令
方法调用指令:invokevirtual、invokeinterface、invokespecial、invokestatic、invokedynamic
以下5条指令用于方法调用:
- invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),支持多态。这也是Java语言中最常见的方法分派方式。
- invokeinterface指令用于调用接口方法,它会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用。
- invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(构造器)、私有方法和父类方法。这些方法都是静态类型绑定的,不会在调用时进行动态派发。
- invokestatic指令用于调用命令类中的类方法(static方法)。这是静态绑定的。
- invokedynamic调用动态绑定的方法,这个是JDK1.7后新加入的指令。用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
- invokespecial
public void invoke1() {
Date date = new Date();
Thread t1 = new Thread();
super.toString();
methodPrivate();
}
private void methodPrivate() {
}
0 new #2 <java/util/Date>
3 dup
4 invokespecial #3 <java/util/Date.<init> : ()V> # 调用Date()构造器
7 astore_1
8 new #4 <java/lang/Thread>
11 dup
12 invokespecial #5 <java/lang/Thread.<init> : ()V> # 调用Thread()构造器
15 astore_2
16 aload_0
17 invokespecial #6 <java/lang/Object.toString : ()Ljava/lang/String;> # 调用父类的toString()方法
20 pop
21 aload_0
22 invokespecial #7 <MethodInvokeReturnTest.methodPrivate : ()V> # 调用类的私有方法
25 return
方法返回指令
方法调用结束前,需要进行返回。方法返回指令是根据返回值的类型区分的。
- 包括ireturn(当返回值是boolean、byte、char、short和int类型时使用),lreturn、freturn、dreturn、areturn。
- 另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。
返回类型 | 返回指令 |
---|---|
void | return |
int(boolean,byte,char,short) | ireturn |
long | lreturn |
float | freturn |
double | dreturn |
reference | areturn |
通过ireturn指令,将当前函数操作数栈 的顶层元素弹出,并将这个元素压入调用者函数的操作数栈中(因为调用者非常关心函数的返回值),所有在当前函数操作数栈中的其他元素都会被丢弃。
如果当前返回的时synchronized方法,那么还会执行一个隐含的monitorexit指令,退出临界区。
最后,会丢弃当前方法的整个帧,恢复调用者的帧,并将控制权转交给调用者。
操作数栈管理指令
如同操作一个普通的数据结构中的堆栈那样,JVM提供了操作数栈管理指令,可以用于直接操作操作数栈的指令。
这类指令包括如下内容:
- 将一个或两个元素从栈顶弹出,并且直接废弃:pop,pop2
- 复制栈顶一个或两个数值并将复制或双份的复制值重新压入栈顶:dup,dup2,dup_x1,dup2_x1,dup_x2,dup2_x2
- 将栈最顶端的两个Slot数值位置交换:swap。Java虚拟机没有提供交换两个64位数据类型(long,double)数值的指令。
- 指令nop,是一个非常特殊的指令,他的字节码位0x00。和汇编语言中的nop一样,它表示什么都不做。这条指令一般可用于调试、占位等。
这些指令属于通用型,对栈的压入或弹出无需指明数据类型。
说明
-
不带_x指令是复制栈顶数据并压入栈顶。包括两个指令,dup和dup2。dup的系数代表要复制的Slot个数。
- dup开头的指令用于复制1个Slot的数据。例如1个int或者1个reference类型数据
- dup2开头的指令用于复制2个Slot的数据。例如1个long,或2个int,或1个int+1个float类型数据
-
带_x的指令是复制栈顶数据并插入栈顶以下的某个位置。共有4个指令,dup_x1,dup2_x1,dup_x2,dup2_x2。对于带_x的复制插入指令,只要将指令的dup和x系数相加,结构即为需要插入的位置。因此
- dup_x1插入位置: 1+1=2,即栈顶2个Slot下面
- dup_x2插入位置: 1+2=3,即栈顶3个Slot下面
- dup2_x1插入位置:2+1=3,即栈顶3个Slot下面
- dup2_x2插入位置:2+2=4,即栈顶4个Slot下面
- pop:将栈顶的1个Slot数值出栈。例如1个short类型数值
- pop2:将栈顶2个Slot数值出栈。例如1个double或者2个int类型的数值
代码:
public long nextIndex() {
return index++;
}
private long index = 0;
0 aload_0 # 将局部变量表中索引为1的位置的元素,加载到操作数栈中,即this的地址
1 dup # 复制一份栈顶元素,即this的地址被复制了一份
2 getfield #2 <MethodInvokeReturnTest.index : J> # 将栈顶this的地址出栈,并将index值0入栈,index是long类型的,注意,占据两个slot,到这里,栈中有3个slot被占用了
5 dup2_x1 # 复制栈顶元素,即index所占用的两个slot,并插入从栈顶往下数的第三个位置,到这里,栈顶的前2个slot是index值0占用,第三个slot是this的地址,第4和第5个slot还是index值0占用
6 lconst_1 # 在入栈long类型的1到操作数栈中
7 ladd # 将操作数栈顶两个元素出栈,并将结果1入栈
8 putfield #2 <MethodInvokeReturnTest.index : J> # 将栈顶两个元素出栈了
11 lreturn # 将栈顶元素出栈,此时栈顶元素是0
控制转移指令
程序流程离不开条件控制,为了支持条件跳转,虚拟机提供了大量字节码指令,大体上可以分为:1.比较指令、2条件跳转指令、3.比较条件跳转指令、4.多条件分支跳转指令、5.无条件跳转指令等。
比较指令
- 比较指令的作用就是比较栈顶两个元素的大小,并将比较结果入栈。
- 比较指令有:dcmpg、dcmpl、fcmpg、fcmpl、lcmp(d表示double,l表示long)
- 对于double和float类型的数字,由于NaN的存在,各有两个版本的比较指令。以float为例,有fcmpg和fcmpl两个指令,它们的区别在于在数字比较时,若遇到NaN,处理结果不同
- 指令dcmpl和dcmpg也是类似的,根据其命名可以推测其含义。
- 指令lcmp针对long型整数,由于long型整数没有NaN值,故无需准备两套指令。
举例:指令fcmpg和fcmpl都从栈中弹出两个操作数,并将它们做笔记,设栈顶的元素为v2,栈顶顺位第2位的元素为v1,若v1=v2,则压入0;若v1>v2则压入1;若v1<v2则压入-1。
两个指令的不同之处在于,如果遇到NaN值,fcmpg会压入1,而fcmpl会压入-1。
条件跳转指令
条件跳转指令通常和比较指令结合使用。在条件跳转指令执行前,一般可以先用比较指令进行栈顶元素的准备,然后进行条件跳转。条件跳转指令有:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnonnull。这些指令都接收两个字节的操作数,用于计算跳转的位置(16位符号整数作为当前位置的offset)。
它们的统一含义为:弹出栈顶元素,测试它是否满足某一条件,如果满足条件,则跳转到给定位置。
指令 | 说明 |
---|---|
ifeq | 当栈顶int类型数值等于0时跳转 |
ifne | 当栈顶int类型数值不等于0时跳转 |
iflt | 当栈顶int类型数值小于0时跳转 |
ifle | 当栈顶int类型数值小于等于0时跳转 |
ifgt | 当栈顶int类型数值大于0时跳转 |
ifge | 当栈顶int类型数值大于等于0时跳转 |
ifnull | 为null时跳转 |
ifnonnull | 不为null时跳转 |
-
与前面运算规则一致:
- 对于boolean、byte、char、short类型的条件分支比较操作,都是使用int类型的比较指令完成
- 对于long、float、double类型的条件分支比较操作,则会先执行相应类型的比较运算指令,运算指令会返回一个整型值到操作数栈中,随后再执行int类型的条件分支比较操作来完成整个分支跳转。
- 由于各类型的比较最终都会转为int类型的比较操作,所以Java虚拟机提供的int类型的条件分支指令是最为丰富和强大的。
代码:
public void compare1() {
int a = 0;
if (a == 0) {
a = 10;
} else {
a = 20;
}
}
字节码:
0 iconst_0 # 将0让如操作数栈中
1 istore_1 # 将操作数栈中元素出栈,存储在局部变量表索引为1的位置
2 iload_1 # 将局部变量表索引为1的位置id元素,放一份到操作数栈中
3 ifne 12 (+9)# 判断栈顶元素是否不等于0,如果不等于0跳转到12行指令,即bipush 20这行指令
6 bipush 10 # 如果栈顶元素等于0,则将操作数栈顶放入元素10
8 istore_1 # 将操作数栈顶元素10出栈后,放在局部变量表索引为1的位置
9 goto 15 (+6)# 无条件跳转指令,直接跳转到操作指令15的位置,即return指令被执行,程序结束
12 bipush 20 # 将操作数栈顶放入元素20
14 istore_1 # 将操作数栈顶元素出栈,并存储在局部变量表中索引为1的位置,即将20放入局部变量表索引为1的位置处
15 return # 方法返回
代码:
public boolean compareNull(String str) {
if (str == null) {
return true;
} else {
return false;
}
}
字节码:
0 aload_1 # 将操作数栈中索引为1的位置的元素,放入操作数栈中
1 ifnonnull 6 (+5) # 判断栈顶元素是否不等于null,如果条件成立,跳转到6指令执行,即操作数栈中方法0元素
4 iconst_1 # 条件不成立(即等于null),操作数栈顶放入元素1
5 ireturn # 返回1,即栈顶元素
6 iconst_0 # 条件成立,栈顶放入元素0
7 ireturn # 返回0,即栈顶元素
代码:
public void compare2() {
float f1 = 9;
float f2 = 10;
System.out.println(f1 < f2);
}
字节码:
0 ldc #2 <9.0> # 从常量池中加载9.0到操作数栈顶
2 fstore_1 # 栈顶元素出栈,并存入局部变量表索引为1的位置
3 ldc #3 <10.0> # 从常量池中加载10.0到操作数栈顶
5 fstore_2 # 栈顶元素出栈,并存入局部变量表索引为2的位置
6 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;>
9 fload_1 # 将局部变量表索引为1的位置的元素,复制一份放入操作数栈顶
10 fload_2 # 将局部变量表索引为2的位置的元素,复制一份放入操作数栈顶
11 fcmpg # 比较栈顶两个float类型的元素的大小,若第二位元素小于第一位元素,这操作数栈中让如-1
12 ifge 19 (+7) # 用栈顶元素-1与0比较是否大于等于?,如果不成立,则不跳转。
15 iconst_1 # 将元素1放入操作数栈顶
16 goto 20 (+4) # 无条件跳转到指令20去执行
19 iconst_0 # 将元素0放入操作数栈顶
20 invokevirtual #5 <java/io/PrintStream.println : (Z)V>
23 return # 方法执行结束,返回
代码:
public void compare3() {
int i1 = 10;
long l1 = 20;
System.out.println(i1 < l1);
}
字节码:
0 bipush 10 # 将常量10压入操作数栈中
2 istore_1 # 将栈顶元素10出栈,放入局部变量表索引为1的位置
3 ldc2_w #6 <20> # 将常量池中元素20,压入操作数栈中
6 lstore_2 # 将栈顶元素20出栈,放入局部变量表索引为2的位置
7 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;>
10 iload_1 # 将局部变量表中索引为1的位置的元素,复制一份,放入操作数栈中
11 i2l # 将栈顶元素(int类型)转换成long类型
12 lload_2 # 将局部变量表中索引为2的位置的元素,复制一份,放入操作数栈中
13 lcmp # 将栈顶两个元素出栈,比较的结果入栈。由于栈顶第一个元素20,第二个元素10,第二个元素小于第一个元素,所以栈顶放入-1
14 ifge 21 (+7) # 栈顶元素-1大于0是不成立的,所以直接往下执行第17条指令。否则跳转到21条指令
17 iconst_1 # 将1压入操作数栈顶
18 goto 22 (+4) # 无条件跳转到22行指令,去执行
21 iconst_0 # 将0压入操作数栈顶
22 invokevirtual #5 <java/io/PrintStream.println : (Z)V>
25 return # 方法执行完成,返回
比较条件跳转指令
比较条件跳转指令类似于比较指令和条件跳转指令的结合体,它将比较和跳转两个步骤合二为一。
这类指令有:if_icmpeg、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。其中指令助记符加上”if_”后,以字符”i”开头的指令针对int型整数操作(也包括short和byte类型),以字符”a”开头的指令表示对象引用的比较。
指令 | 说明 |
---|---|
if_icmpeq | 比较栈顶两int类型数值大小,当前者等于后者时跳转 |
if_icmpne | 比较栈顶两int类型数值大小,当前者不等于后者时跳转 |
if_icmplt | 比较栈顶两个int类型数值的大小,当前者小于后者时跳转 |
if_icmple | 比较栈顶两个int类型数值的大小,当前者小于等于后者时跳转 |
if_icmpgt | 比较栈顶两个int类型数值的大小,当前者大于后者时跳转 |
if_icmpge | 比较栈顶两个int类型数值的大小,当前者大于等于后者时跳转 |
if_acmpeq | 比较两个引用类型数值,当结果相等时跳转 |
if_acmpne | 比较两个引用类型数值,当结果不相等时跳转 |
这些指令都接收两个字节的操作数作为参数,用于计算跳转的位置。同时在执行指令时,栈顶需要准备两个元素进行比较。指令执行完成后,栈顶的这两个元素被清空,且没有任何数据入栈。如果预设条件成立,则执行跳转,否则,继续执行下一条语句。
代码:
public void ifCompare1() {
int i = 10;
int j = 20;
System.out.println(i < j);
}
字节码:
0 bipush 10 # 将常量10压入操作数栈中
2 istore_1 # 将栈顶元素10从操作数栈中取出,放入局部变量表中索引为1的位置
3 bipush 20 # 将常量20压入操作数栈中
5 istore_2 # 将常量20出栈,存入局部变量表索引为2的位置
6 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;>
9 iload_1 # 将局部变量表中索引为1的位置元素复制一份放入操作数栈中
10 iload_2 # 将局部变量表中索引为2的位置元素复制一份放入操作数栈中
11 if_icmpge 18 (+7) # 比较操作数栈顶两个元素,当前者(10)大于后者(20)时,跳转到18指令执行,否则顺序往下执行
14 iconst_1 # 显然条件不成立,不会跳转,因此,将常量1压入操作数栈中
15 goto 19 (+4)# 无条件跳转到指令19
18 iconst_0 # 条件成立时,会跳转到这里,因此,将常量0压入操作数栈中(0代表false)
19 invokevirtual #5 <java/io/PrintStream.println : (Z)V> # 输出栈顶元素
22 return # 方法结束
代码:
public void ifCompare2() {
short s1 = 9;
byte b1 = 10;
System.out.println(s1 > b1);
}
字节码:
0 bipush 9 # 将常量9放入操作数栈顶
2 istore_1 # 栈顶元素9出栈,放入局部变量表中索引为1的位置
3 bipush 10 # 将常量10放入操作数栈顶
5 istore_2 # 栈顶元素10出栈,放入局部变量表索引为2的位置
6 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;>
9 iload_1 # 将局部变量表索引为1的位置的元素,放入操作数栈顶
10 iload_2 # 将局部变量表索引为2的位置的元素,放入操作数栈顶
11 if_icmple 18 (+7) # 比较栈顶两个元素,但前者(9)小于后者(10)时,跳转的指令18
14 iconst_1 # 将常量1放入操作数栈顶
15 goto 19 (+4) #无条件跳转到指令19去执行
18 iconst_0 # 将常量0放入操作数栈顶
19 invokevirtual #5 <java/io/PrintStream.println : (Z)V> # 输出操作数栈顶的元素
22 return # 方法结束,返回
代码:
public void ifCompare3() {
Object obj1 = new Object();
Object obj2 = new Object();
System.out.println(obj1 == obj2);
System.out.println(obj1 != obj2);
}
字节码:
0 new #8 <java/lang/Object> # 创建对象(即在堆中分配空间,在操作数栈中放入一个地址值,该地址值为堆空间对象地址
3 dup # 操作数栈顶的地址复制一份
4 invokespecial #1 <java/lang/Object.<init> : ()V>
7 astore_1 # 栈顶元素出栈,并存入局部变量表索引为1的位置
8 new #8 <java/lang/Object> # 创建对象,并在操作数栈顶放入堆空间的地址值
11 dup # 复制栈顶元素的地址值,并压入操作数栈中
12 invokespecial #1 <java/lang/Object.<init> : ()V>
15 astore_2 # 操作数栈顶元素出栈,并将地址值存入局部变量表索引为2的位置
16 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;>
19 aload_1 # 将局部变量表索引为1的位置的地址值,复制一份存入操作数栈顶
20 aload_2 # 将局部变量表索引为2的位置的地址值,复制一份存入操作数栈顶
21 if_acmpne 28 (+7) # 比较操作数栈顶的两个元素,如果不相等,则跳转。显然不相等,跳转
24 iconst_1 # 操作数栈顶放入常量1
25 goto 29 (+4) # 无条件跳转到指令29
28 iconst_0 # 操作数栈顶放入常量0
29 invokevirtual #5 <java/io/PrintStream.println : (Z)V>
32 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;>
35 aload_1
36 aload_2
37 if_acmpeq 44 (+7)
40 iconst_1
41 goto 45 (+4)
44 iconst_0
45 invokevirtual #5 <java/io/PrintStream.println : (Z)V>
48 return
多条件分支跳转指令
多条件分支跳转指令是专为swtich-case语句设计的,主要有tableswitch和lookupswitch。
指令名称 | 描述 |
---|---|
tableswitch | 用于switch条件跳转,case值连续 |
lookupswitch | 用于switch条件跳转,case值不连续 |
从助记符上看,两者都是switch语句的实现,它们的区别:
- tableswitch要求多个条件分支是连续的,它内部只存放起始值和终止值,以及若干个跳转偏移量,通过给定的操作数index,可以立即定位到跳转偏移量位置,因此效率比较高。
- 指令lookupswitch内部存放着各个离散的case-offset对,每次执行都要搜索全部的case-offset对,找到匹配的case值,并根据对应的offset计算跳转地址,因此效率比较低。
代码:
public void switch1(int select) {
int num;
switch (select) {
case 1:
num = 10;
break;
case 2:
num = 20;
break;
case 3:
num = 30;
break;
default:
num = 40;
}
}
字节码:
0 iload_1 # 将局部变量表,索引为1的位置的元素,放入操作数栈顶
1 tableswitch 1 to 3 # 条件范围的起始值为1,结束值为3,判断栈顶元素的值,加入栈顶元素传入的是2,则会执行2对应的指令34
1: 28 (+27) # 如果匹配值1,则执行指令28
2: 34 (+33) # 如果匹配值2,则执行指令34
3: 40 (+39) # 如果匹配值3,则执行指令40
default: 46 (+45) # 如果没有匹配到,则执行指令 46
28 bipush 10
30 istore_2
31 goto 49 (+18)
34 bipush 20 # 将常量20放入操作数栈顶
36 istore_2 # 将操作数栈顶元素出栈,并存入局部变量表索引为2的位置
37 goto 49 (+12) # 执行无条件跳转,到指令49
40 bipush 30
42 istore_2
43 goto 49 (+6)
46 bipush 40
48 istore_2
49 return
代码:
public void switch2(int select) {
int num;
switch (select) {
case 100:
num = 10;
break;
case 200:
num = 20;
break;
case 1000:
num = 30;
break;
default:
num = 40;
}
}
字节码:
0 iload_1
1 lookupswitch 3 # 由于数值不连续,这里用的是lookupswitch指令,其他的同上
100: 36 (+35)
200: 42 (+41)
1000: 48 (+47)
default: 54 (+53)
36 bipush 10
38 istore_2
39 goto 57 (+18)
42 bipush 20
44 istore_2
45 goto 57 (+12)
48 bipush 30
50 istore_2
51 goto 57 (+6)
54 bipush 40
56 istore_2
57 return
指令lookupswitch处理的是离散的case值,但是处于效率考虑,将case-offset对按照case值大小,给定index对,需要查找与index相等的case,获得其offset,如果找不到则跳转到default。
无条件跳转指令
目前主要的无条件跳转指令为goto。指令goto接收两个字节的操作数,共同组成一个带符号的整数,用于指定指令的偏移量,指令执行的目的就是跳转到偏移量给定的位置处。
如果指令偏移量太大,超过了双字节的带符号整数的范围,则可以使用指令goto_w,它和goto有相同的作用,但是它接收4个字节的操作数,可以表示更大的地址范围。
指令jsr、jsr_w、ret虽然也是无条件跳转的,但主要用于try-finally语句,且已经被虚拟机逐渐废弃,故不在这里介绍着两个指令。
指令名称 | 描述 |
---|---|
goto | 无条件跳转 |
goto_w | 无条件跳转(宽索引) |
jsr | 跳转至指定16位offset位置,并将jsr下一条指令地址压入栈顶 |
jsr_w | 跳转至指定32位offset位置,并将jsr_w下一条指令地址压入栈顶 |
ret | 返回至由指定的局部变量所给出的指令位置(一般与jsr、jsr_w联合使用) |
代码:
public void whileInt() {
int i = 0;
while (i < 100) {
String s = "atguigu.com";
i++;
}
}
字节码:
0 iconst_0 # 将常量0放入操作数栈顶
1 istore_1 # 将操作数栈顶元素出栈,放入局部变量表索引为1的位置
2 iload_1 # 将局部变量表索引为1的位置的元素,放入操作数栈的栈顶
3 bipush 100 # 将常量100放入操作数栈顶
5 if_icmpge 17 (+12) # 栈顶两个元素出栈,如果前者(1)大于后者(100)时,跳转。由于1不大于100,不执行跳转
8 ldc #9 <atguigu.com> # 加载字符串到操作数栈顶
10 astore_2 # 将操作数栈顶元素,存入局部变量表索引为2的位置
11 iinc 1 by 1
14 goto 2 (-12) # 执行条件,跳到指令2的位置
17 return
这里的循环操作,使用条件判断指令
if_icmpge
和无条件跳转指令
goto
模拟实现的。