在java代码中,可以使用if, if-else, while, do-while, for以及switch语句来指定基本的控制流。当java代码经过编译后,这些条件控制语句就会转换成对应的条件分支指令。对于所有条件判断指令,JVM都是先从栈中弹出该指令所需要的操作数,然后进行比较,如果比较失败,JVM将执行条件分支语句后面的指令,如果比较成功则会使用指令后的两个操作数字节产生的一个带符号的16位偏移量,JVM给当前的PC寄存器加上这个偏移量来获取目标地址。目标地址必须指向同一规格方法中的一条指令的操作嘛。程序会继续从目标地址开始运行。
对于比较成功的情况,ASM并没有上面所说的那么复杂,我们只需要调用指令的时候传入一个Label对象即可,而这个对象所表示的地址就是指令比较成功后所需要跳转的地址。
JVM的比较指令主要分为以下几类:
- 整数与0比较的条件分支
- 整数与整数比较的条件分支
- long,float,double类型的比较
- 对象与null类型的比较的条件分支
- 对象引用之间的比较的条件分支
这个多用于boolean类型的判断,boolean在java虚拟机中是以int类型表示的,所以当栈顶元素是boolean类型的时候可以使用和0比较做条件分支,这个类别的指令会从栈顶弹出一个int类型的值,然后和0比较,根据结果跳转到指定目录。这一类型指令包括:ifeq(等于0),ifne(不等于0),iflt(小于0),ifle(小于等于0),ifgt(大于0),ifge(大于等于0)。
这个条件分支操作从栈顶弹出两个整数类型的操作数,然后根据比较结果判断是否跳转到指定位置if_icmpeq, if_icmpne, if_icmplt, if_icmple, if_icmpgt, if_icmpge。分别是判断两个整数是否相等,不相等,小于,小于等于,大于,大于等于。
这是对long,float和double类别的比较,从栈顶弹出两个元素进行比较,并且将比较结果压入栈。如果第一个值大于第二个值压入1,等于压入0,小于压入-1。这个和上面的条件分支比较区别是,他并不做跳转,所以一般来说想对long,float,double三种类型做条件分支的话都要配合整数与0比较的条件分支 配合使用。虽然我们在写类似于if(i>j)这样的代码的时候,无论i和j是什么类型都没什么差别,但是在所产生的字节码不仅使用的指令不同,连逻辑也有可能不一样。这个类型的相关指令是lcmp,fcmpg,fcmpl,dcmpg,dcmpl
这个类别的条件分支会从栈顶弹出一个引用类型的值,如果该值为null(ifnull指令)或者不为null(ifnonnull指令),则跳转到指定位置。这个类别有两个指令分别是ifnull和finonnull。
这个类别的条件分支会从栈顶弹出两个引用类型的值作为操作数,如果两个操作数相等(if_acmpeq)或者不相等(if_acmpne),则跳转到指定位置。这个类别有两个指令分别是if_acmpeq和if_acmpne。这里判断是否相等是判断两个引用所指向的对象是否是同一个,这里相当于java代码中对两个对象用“==”符合判断,很多时候我们用equals来判断是否相等,这种方式实际上是方法调用操作。
以上几种都是条件跳转,类似于java源代码中实现if跳转功能能,还有一个无条件跳转就是goto指令,这个指令可以跳到任意字节码指令位置。通过结合条件跳转和无条件跳转就能实现循环语句了,比如:
1 iconst_0 2 istore_1 [i] 3 goto 8 4 getstatic java.lang.System.out : java.io.PrintStream 5 iload_1 [i] 6 invokevirtual java.io.PrintStream.println(int) : void 7 iinc 1 1 [i] 8 iload_1 [i] 9 iconst_5 10 if_icmplt 4 11 ...
上面代码中 首先iconst_0 和istore_1 指令创建一个局部变量i,并且初始值为0 ;goto 8 跳转到条件判断;进入19 位置,iload_1 和 iconst_5 往栈中压入i 和5 ;if_icmplt 4 将栈中的i 和5 弹出栈,判断如果i 小于5 则跳到4 的位置,否则继续往下执行11;从4 到6 就是就是将i 输出到控制台,当指令执行完6 之后又进入了条件判断。上面指令对应的代码如下:
int i = 0; while(i < 5) System.out.println(i++);