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值而失败,编译器都会根据不用的操作类型来选择不同的比较指令。

posted @ 2019-06-04 22:25  lwli  阅读(241)  评论(0编辑  收藏  举报