java虚拟机规范(se8)——java虚拟机的编译(二)
3.3 算术运算
java虚拟机通常在操作数栈上进行算术运算(例外情况是iinc指令,它直接增加一个局部变量的值)。例如下面的align2grain()方法,它的作用是将int值对齐到2的指定次幂:
int align2grain(int i, int grain) { return ((i + grain-1) & ~(grain-1)); }
算术运算的操作数是从操作数栈中弹出的,运算结果会压回操作数栈。因此,算术子计算的结果可以作为嵌套计算的操作数。例如。~(grain-1)的计算结果就是被这样使用的:
5 iload_2 // Push grain 6 iconst_1 // Push int constant 1 7 isub // Subtract; push result 8 iconst_m1 // Push int constant -1 9 ixor // Do XOR; push result
首先使用局部变量2的值和一个int类型的立即数1来计算grain-1的值。这些操作数被操作数栈弹出,然后他们的差值压回操作数栈,这个差值可以立即作为一个操作数被ixor指令使用(回想,~x == -1 ^ x)。同样的,ixor指令的计算结果成为接下来iand指令的一个操作数。
这个方法的代码如下:
Method int align2grain(int,int) 0 iload_1 1 iload_2 2 iadd 3 iconst_1 4 isub 5 iload_2 6 iconst_1 7 isub 8 iconst_m1 9 ixor 10 iand 11 ireturn
3.4 访问运行时常量池
许多数值类型常量,以及对象,字段和方法是通过当前类的运行时常量池来访问的。对象的访问在3.8节讨论。数据类型为int,long,float和double以及String类示例的引用通过ldc,ldc_w和ldc2_w来管理。
ldc和ldc_w指令用于访问运行时常量池中除了double和long类型的值(但是包括String类型实例)。当使用的运行时常量池的项目过多时(多余256个,一个字节能表示的范围),需要使用ldc_w来代替。ldc2_w用于访问所有double和long类型的值,它没有对应的非宽版本,即没有ldc2指令。
整型常量类型byte,char,short,以及小的int值,可能使用bipush,sipush或者iconst_<i>指令来编译。某些小的浮点型常量也可以使用fconst_<f>和dconst_<d>指令来编译。
在所有这些情况下,编译都是非常直观的,例如,下面这些常量:
void useManyNumeric() { int i = 100; int j = 1000000; long l1 = 1; long l2 = 0xffffffff; double d = 2.2; ...do some calculations... }
编译后:
Method void useManyNumeric() 0 bipush 100 // Push small int constant with bipush 2 istore_1 3 ldc #1 // Push large int constant (1000000) with ldc 5 istore_2 6 lconst_1 // A tiny long value uses small fast lconst_1 7 lstore_3 8 ldc2_w #6 // Push long 0xffffffff (that is, an int -1) // Any long constant value can be pushed with ldc2_w 11 lstore 5 13 ldc2_w #8 // Push double constant 2.200000 // Uncommon double values are also pushed with ldc2_w 16 dstore 7 ...do those calculations...
3.5 更多控制结构的示例
for语句的编译已经在前面的3.2节中展示过了。绝大多数java编程语言的其他控制结构(if-then-else,do,while,break和continue)也同样使用明细的方式来编译。switch语句的编译在单独的章节(3.10)介绍,异常的编译(3.12),finally从句的编译(3.13)。
作为进一步的示例,while循环以一种明显的方式编译,尽管Java虚拟机提供的特定控制传输指令因数据类型的不同而不同。像往常一样,对int类型的数据有更多的支持,例如:
void whileInt() { int i = 0; while (i < 100) { i++; } }
编译为:
Method void whileInt() 0 iconst_0 1 istore_1 2 goto 8 5 iinc 1 1 8 iload_1 9 bipush 100 11 if_icmplt 5 14 return
注意,while语句的条件判断(使用if_icmplt指令实现)位于Java虚拟机代码循环的的底部。(在前面的spin例子中也是如此)。位于循环底部的条件判断强制使用goto指令,以便在循环的第一次迭代之前进行条件判断。如果不满足条件,并且循环体从未进入,那么这个额外的指令就被浪费了。不过while循环通常用于期望循环体会执行的场景中。对于后续的迭代,将判断条件放在循环的底部每次循环时都会节省一条Java虚拟机指令:如果判断条件位于循环的顶部,则循环体将需要一条尾随的goto指令才能回到顶部。
使用其他数据类型的控制结构都使用相同的方式来编译,但是必须使用对应数据类型的指令。这会导致一些效率不高的代码,因为需要更多的java虚拟机指令,例如:
void whileDouble() { double i = 0.0; while (i < 100.1) { i++; } }
编译成:
Method void whileDouble() 0 dconst_0 1 dstore_1 2 goto 9 5 dload_1 6 dconst_1 7 dadd 8 dstore_1 9 dload_1 10 ldc2_w #4 // Push double constant 100.1 13 dcmpg // To compare and branch we have to use... 14 iflt 5 // ...two instructions 17 return
每一种浮点类型有两个比较指令:float的fcmpl和fcmpg,double的dcmpl和dcmpg。这些指令语义相似,仅仅在对待NaN变量时有所区别。NaN是无序的,所以只要有一个操作数是NaN,浮点指令的比较结果都会失败。无论比较操作是否会因为遇到NaN值而失败,编译器都会根据不用的操作类型来选择不同的比较指令。