引子:一个程序无论多么的复杂,站在宏观角度看其实都是在三种控制流中执行:顺序执行,条件执行,循环执行。顺序执行就是一条一条的语句按照语句所在位置的顺序依次执行。条件执行就是根据条件的真假有选择的执行某一段代码,而被选择要执行的代码段是顺序执行的。 循环执行就是根据条件重复执行一段代码,而被重复执行的代码段也是顺序执行的。可以看到顺序执行是基础,另外两个是在顺序执行的基础上加了一些控制条件。之前实现的Mini JVM 已经实现了顺序执行的控制流了,现在就来看一下如何实现条件判断和循环控制流。
1. 总览
首先先来看一下这个版本的Mini JVM所要支持的控制流:
没错,红色框的内容就是要实现的条件判断和循环控制流
在具体实现之前,我们先来回顾一下我们整天都在写的if,while/for
的执行流程是怎么样的。对于if
语句,首先先要判断一下条件,如果为true
则执行if
代码块,如果为false
则执行if之后的代码块;对于while/for
语句,首先先要判断一下条件,如果为true
则执行while/for
代码块,等while/for
代码块执行完之后再判断一下条件,直到条件不满足时再执行while/for
之后的代码块。从上面的描述可以看到,其实条件控制流和循环控制流是一会是,都是根据条件执行代码段,只是循环控制流有可能会反复执行而已。
2. 实现
知道了条件控制流和循环控制流的本质之后, 我们再来了看一下Java编译器把这两个控制流编译成了什么命令,让其可以实现对应的逻辑。
2.1 条件控制流
可以看到,打红框框的就是if语句被编译之后的字节码,但是有同学就问了这一串数字是啥呀....?!别急,我帮这串"天书"翻译一下:
这串翻译之后就是上面那个样子(其实在之前执行引擎那篇文章中已经提到过,这里再补充一下,这些数字所代表的具体意义是在oracle jvm虚拟机规范中的操作码中有定义的,所以参照那个文档就可以对这个看似无意义的数字进行翻译)。 从这些命令当中可以看到有两个命令是之前没有遇见到过的,这两个命令也是实现if
控制流,甚至while/for
控制流的关键。一个是if_icmpge
,一个是goto
。
通过名字就可以知道,if_icmpge
就是一个判断命令, 用于比较栈顶两 int 型数值大小,当结果大于等于 0 时跳转。同理也可以猜到类似的命令还有if_icmpgt,if_icmpne
等。至于其他的类型也有类似的命令. 知道了原理之后要实现这个命令也很简单, 只要计算表达式的值是否符合跳转的条件, 如果符合条件则计算下一个命令的偏移量然后将下一个命令的值置为这个新的偏移量(如果了解操作系统的话,就类似于操作系统中函数的调用, 实际上就是改变pc(程序计数器)的值, 让其指向下一条指令的位置)。
而至于goto
命令,顾名思义就是跳转,这个命令的实现更加的简单,都不需要计算,直接"无脑"跳转就好了。
所以依赖于一批if_*
命令和一个goto
指令就实现了if
控制流。
2.2 循环控制流
知道了条件控制流之后,以及前面已经介绍过了循环控制流就可能不断执行的条件控制流,所以对于循环控制流的实现也很容易想到了。还是照旧让我们来看一下Java编译器把循环控制流编译成了什么命令:
嗯, 还是一串数字,再来看一下翻译之后的命令:
上面就是for循环的命令,可以看到其实它和条件的判断的命令根本没有什么区别,就是使用if_*
指令集和goto
命令来实现的。 唯一的区别就是goto
语句会跳到goto
前面的命令,这样就会再次执行if_icmpgt
命令,也正是这样就实现了循环. 所以实际上可以把条件判断控制流看成是特殊的循环控制流, 也即条件控制流是之会执行一次的循环控制流。
如果细心的同学仔细观察可以看到这个有一个地方比较有意思, 就是goto
命令的两个偏移量是0xff
和0xf3
,通过这个值计算出来的偏移量加上goto
命令本身的偏移量得到的最后的偏移量实际上是会大于2个字节的能表示的最大值的,这个时候产生的溢出刚好可以让下一个命令的偏移量移到goto
命令之前, 从而实现循环。
所以不管条件还是循环,编译成字节码之后, 在jvm看来都是一样的,没有什么区别。
3. 总结
如果同学之前看过或者学过一些汇编的知识,其实可以发现jvm这个条件,循环的实现和汇编的条件、循环跳转的实现是很相似的。都是通过一些类似if_*
的命令和一个goto
命令实现的。所以不管你这个语言的特性多么强大,语法可以写的多么简洁,多么花哨,到了指令这一层其实都是大同小异的。所以说即使是写java这种高级语言,了解一些底层的东西也还是有必要的。
这个版本的Mini JVM的代码也已经提交,有兴趣的,可以看看。