深入虚拟机笔记之控制流

第16章 控制流

    条件分支:在java源码中,可以在一个方法中使用if、if-else、while、do-while、for和switch语句来指定基本的控制流。当把源码转换成字节码时,除了switch语句外,java编译器使用同样的操作码集。每一种操作码都会从栈顶弹出一个或者两个值,然后进行比较;从栈中弹出一个值的操作码把该值与0进行比较;从栈中弹出两个值得操作码对这两个值进行比较。如果比较成功(成功在不同操作码中的定义不同),java虚拟机将按照由比较操作码的操作数提供的偏移量执行分支或者调整操作。

    对于所有的条件分支,java虚拟机都通过同样的过程来决定下一条将要执行的指令。虚拟机首先执行由操作码所决定的比较;如果比较失败,虚拟机将继续执行条件分支语句后面的指令;如果比较成功,虚拟机将会使用紧随操作码后的两个操作数字节来产生一个带符号的16位偏移量;虚拟机给当前线程的PC寄存器(程序计数器)加上这个偏移量(条件分支操作码的地址)来获取目标指令地址。目标地址必须指向同一个方法中的一条指令。程序会继续从目标地址开始运行。

    if操作码从栈顶弹出两个整数,将它们进行比较;如果比较成功,就执行分支操作。其中value2是栈顶端的值,value1是value2下面的值,即第一个弹出的是比较符右边的值,第二个弹出的是比较符左边的值。

    这些操作码在比较byte、short和char时,java虚拟机首先将它们转换为int类型的值,然后在对int类型进行比较。

 

    操作码对long、float、double进行比较操作时,从栈顶弹出两个整数,将它们进行比较;这些操作码本身并不会执行分支操作,而是把代表比较结果的int类型值(0表示相等、1表示大于、-1表示小于)压入栈中。然后使用int类型的条件分支操作码进行实际分支跳转。

    用于比较float和double的操作码(fcmpg、fcmpl、dcmpg、dcmpl),不同之处在于处理NaN的方式。在java虚拟机中,如果进行比较的值之一是NaN,浮点值比较通常会失败。当至少有一个比较值是NaN时,fcmpg、dcmpg指令将1压入栈,而fcmpl、dcmpl将-1压入栈。

    对象引用和null比较时,操作码从栈顶弹出对象引用,将其与null进行比较;如果比较成功,虚拟机将会执行分支操作。

    对象引用比较时,操作码从栈中弹出两个对象引用,对它们进行比较;只有两种比较结果:如果相等,说明它们指向堆中的同一个对象;如果不相等,说明它们指向不同对象。如果比较成功,虚拟机执行分支操作。

    无条件分支操作:goto指令。

    为了执行goto指令,虚拟机首先根据紧随指令的两个操作数字节,得出一个带符号的16位偏移量(goto_w指令需要4字节得出带符号的32为偏移量),虚拟机把得到的偏移量加到当前线程的PC寄存器上;最后得到的地址必须指向当前方法中的一条指令的操作码位置。虚拟机将会在这条指令处继续执行。

    使用表的条件分支:

    条件分支和无条件分支这些控制流在java源码中以if、if-else、while、do-while、for语句表示,这些操作码也能用来表述switch语句,但java虚拟机的指令集为switch语句专门设计了两个操作码:tableswitch和lookupswitch。

    tableswitch和lookupswitch指令都包含一个默认的分支偏移量和一组可变长度的“case值/分支偏移量”对。这两条指令都会将键值从栈中弹出,它们会把键值和所有的case值进行比较;如果发现匹配项,则取与该case值相关的程序分支偏移量,如果没有发现匹配项,则取默认的程序分支偏移量。

    tableswitch和lookupswitch之间的不同之处在于,它们采用不同的方法指定case值。指令lookupswitch比tableswitch使用的范围更广,但tableswitch的效率更高。这两条指令后面都有0-3个填充字节,每个填充字节的内容都是0,这是为了使紧随在填充字节后面的字节能以4字节的整数倍(从方法开头算起)位置处开始(整个java虚拟机指令集中仅有的考虑了边界对齐的多字节指令)。

    操作码lookupswitch之后是0-3个填充字节和4字节的默认分支偏移量;接着是4字节的值(npairs),它指明了指令后附带的“case值/分支偏移量”对的数量;键值是一个int类型值(说明java语言中的switch语句需要一个类型为byte、short、char和int的键值表达式),与每一个case值相关的程序分支偏移量都是一个4字节的偏移量;“case值/分支偏移量”对必须按照case值递增的顺序依次出现。

    操作码tableswitch之后是0-3个填充字节和4字节的默认分支偏移量,接着是8字节的低、高int类型值。低、高值指明了包含在本tableswitch指令中的case值范围。在低、高值后面的是程序分支偏移量跳转表,跳转表项数为(高值 - 低值 + 1),跳转表内容为:高值、低值以及介于高值和低值之间的每一个整数case值对应的程序分支偏移量,排列顺序为case低值到高值的递增序列,序列内容为case对应的程序分支偏移量,没有对应分支偏移量的case值使用默认的分支偏移量,索引为从0开始的跳转表。

    当java虚拟机遇到一条lookupswitch指令时,它必须把键值与与每个case值进行比较,直到遇到下列情况之一时才结束查找:发现匹配的值、检索到case值大于键值(“case/分支”对按case值递增)、所有case值均检索完毕。如果没有找到匹配的值,使用默认的分支偏移量。

    当java虚拟机遇到一条tableswitch指令时,它会简单地检查键值是否位于高值和低值之间;如果不再此范围内,则使用默认的分支偏移量;如果在此范围之内,虚拟机将会选择跳转表中处于(键值 - 低值)位置处的分支偏移量。通过这种方法,虚拟机能够确定适当的程序分支,而无需对每一个case值进行检查。tableswitch适用于case值是连续的情形,可以缩短指令长度并提高效率。

  

猜你喜欢

转载自jaesonchen.iteye.com/blog/2289727