java虚拟机规范(se8)——java虚拟机的编译(一)
第三章 java虚拟机的编译
java虚拟机是设计用来支持java编程语言的。Oracle的JDK软件包含了一个将Java源代码编译成java虚拟机指令集的编译器,以及一个用于java虚拟机本身的运行时系统。了解编译器如何使用java虚拟机对编译器作者来说是有用的,同样也有助于理解java虚拟机本身。
3.1 示例的格式
本章主要由源代码示例和带注释的Java虚拟机代码清单组成,这些代码是由Oracle的JDK 1.0.2版本中的javac编译器为这些示例生成的。Java虚拟机代码是用非正式的“虚拟机汇编语言”编写的,由Oracle的javap工具生成,随JDK发行版一起发布。您可以使用javap生成其他已编译方法的例子。
如果读者阅读过汇编代码,都应该熟悉示例中的格式。每个指令的格式如下:
<index> <opcode> [ <operand1> [ <operand2>... ]] [<comment>]
<index>是包含该方法的Java虚拟机代码字节的数组中指令的操作码的索引。<index>可以被认为是从方法起始处的字节偏移量。<opcode>是指令操作码的助记符,零或更多<operandN>是指令的操作数。 可选的<comment>以行尾注释语法给出:
8 bipush 100 // Push int constant 100
注释中的一部分是有javap产生的,剩余部分由作者添加的。每条指令前的<index>可以被用于控制转移指令的目标。例如,goto 8这条指令表示跳转到索引为8的指令处执行。需要注意的是,java虚拟机的控制转移指令的实际操作数是当前指令的操作码集合中的地址偏移量,这些操作数会被javap工具按照更容易被人阅读的方式来显示。
或者:
9 invokevirtual #4 // Method Example.addTwo(II)I
3.2 常量、局部变量和控制结构的使用
java虚拟机代码中展示了java虚拟机设计和使用所遵循的一些通用特性。在第一个例子中,我们遇到了许多这样的情况,我们对它们进行了详细的考虑。
spin方法简单的进行了100次空循环:
void spin() { int i; for (i = 0; i < 100; i++) { ; // Loop body is empty } }
编译器可能将其编译为下面的代码:
0 iconst_0 // Push int constant 0 1 istore_1 // Store into local variable 1 (i=0) 2 goto 8 // First time through don't increment 5 iinc 1 1 // Increment local variable 1 by 1 (i++) 8 iload_1 // Push local variable 1 (i) 9 bipush 100 // Push int constant 100 11 if_icmplt 5 // Compare and loop if less than (i < 100) 14 return // Return void when done
spin中的fou循环主要由以下指令来实现:
5 iinc 1 1 // Increment local variable 1 by 1 (i++) 8 iload_1 // Push local variable 1 (i) 9 bipush 100 // Push int constant 100 11 if_icmplt 5 // Compare and loop if less than (i < 100)
bipush指令将100作为int值压入操作数栈,然后if_icmplt指令将操作数栈中的值弹出冰河和i进行比较。如果满足条件(变量i<100),将跳转到索引为5的位置,然后到for循环的开始处进行下一次迭代。否则将继续执行if_icmplt指令后面的指令。
如果spin例子中的循环计数器使用了int职位的数据类型,那么编译后的代码也会随之改成相应的类型。例如,将spin例子int改成double:
void dspin() { double i; for (i = 0.0; i < 100.0; i++) { ; // Loop body is empty } }
编译后的代码为:
Method void dspin() 0 dconst_0 // Push double constant 0.0 1 dstore_1 // Store into local variables 1 and 2 2 goto 9 // First time through don't increment 5 dload_1 // Push local variables 1 and 2 6 dconst_1 // Push double constant 1.0 7 dadd // Add; there is no dinc instruction 8 dstore_1 // Store result in local variables 1 and 2 9 dload_1 // Push local variables 1 and 2 10 ldc2_w #4 // Push double constant 100.0 13 dcmpg // There is no if_dcmplt instruction 14 iflt 5 // Compare and loop if less than (i < 100.0) 17 return // Return void when done
现在指令操作的数据类型是专门针对double的(ldc2_w指令稍后会在本章讨论)。
回想一下,double类型的值将占据两个局部变量,尽管只使用最小的索引值去访问这两个局部变量。这同样对longleix生效。再看一个例子:
double doubleLocals(double d1, double d2) { return d1 + d2; }
变成:
Method double doubleLocals(double,double) 0 dload_1 // First argument in local variables 1 and 2 1 dload_3 // Second argument in local variables 3 and 4 2 dadd 3 dreturn
注意局部变量表使用了一对变量来存储doubleLocals中的double值,这对变量绝不能单独操作。
java虚拟机使用一字节大小的操作码的结果是编译后代码非常紧凑。但是一字节操作码也意味着java虚拟机的指令集非常小。作为折中,java虚拟机并不为每种数据类型提供相等的支持:他们并非完全正交的。
例如,在spin的例子中使用了单独的if_icmplt指令来实现for语句中的int值的比较;然而,java虚拟机指令集中对于double类型并没有单独的指令来实现同样的效果。因此在dspin中比较double类型的值,必须在iflt指令之后使用dcmpg指令。
java虚拟机对于int类型中的大多操作提供了直接支持。这在一定程度上是考虑到了java虚拟机操作数栈和局部变量表的实现效率。当然也考虑了大多数程序都会对int进行频繁操作的原因。对于其他的整型数据只有很少的直接支持。例如,没有byte, char和short版本的store,load和add指令。下面的例子使用short类型重写了spin:
void sspin() { short i; for (i = 0; i < 100; i++) { ; // Loop body is empty } }
下面是为java虚拟机编译的代码,使用对另一种类型(很可能是int)进行操作的指令,在必要时在short和int值之间进行转换,以确保对short的操作结果保持在适当的范围内:
Method void sspin() 0 iconst_0 1 istore_1 2 goto 10 5 iload_1 // The short is treated as though an int 6 iconst_1 7 iadd 8 i2s // Truncate int to short 9 istore_1 10 iload_1 11 bipush 100 13 if_icmplt 5 16 return
Java虚拟机中缺少对byte,char和short类型的直接支持并没有大的问题,因为这些类型的值在内部被提升为int(byte和short被符号扩展为int,char是零扩展)。 因此,可以使用int指令对字节,字符和短数据执行操作。 唯一的额外成本是将int操作的值截断为有效范围。
Java虚拟机对于long和浮点类型(float和double)提供了中等程度的支持,仅缺少条件转移指令部分。