本文翻译自: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html
第三章 java 虚拟机的编译
java 虚拟机是设计用来支持 java 编程语言的. Oracle 的 JDK 软件包含了一个将 Java 源代码编译成 java 虚拟机指令集的编译器, 以及一个用于 java 虚拟机本身的运行时系统. 了解编译器如何使用 java 虚拟机对编译器作者来说是有用的, 同样也有助于理解 java 虚拟机本身. 本章中编号的部分不是规范性的.
注意, 术语 "编译器" 有时用于指从 Java 虚拟机的指令集到特定 CPU 的指令集的转换程序. 这种转换器的一个例子是即时 (just-in-time, JIT) 代码生成器, 它只在加载 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 工具按照更容易被人阅读的方式来显示.
我们在表示运行时常量池索引的操作数的前面加上一个 #符号, 然后接着指令之后有一条注释来标识引用的运行时常量池项, 如下所示:
10 ldc #1 // Push float constant 100.0
或者:
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
Java 虚拟机是面向堆栈的, 大多数操作从 Java 虚拟机当前帧的操作数堆栈中获取一个或多个操作数, 或者将结果推回到操作数堆栈中. 任何时候当一个方法被调用时, 一个新的栈帧就会被创建出来, 同时创建一个新的操作数栈和局部变量表供这个方法使用. 因此在计算的任何一点, 每个控制线程可能存在许多栈帧和相同数量的操作数堆栈, 对应于许多嵌套方法调用. 只有当前帧中的操作数堆栈处于活动状态.
java 虚拟机指令集使用不同的字节码来区分不同的操作数类型, 用于操作各种类型的操作数. spin 方法仅仅操作了 int 类型的值. 它编译后的代码中的指令都选择了针对 int 型的数据类型操作指令(iconst_0, istore_1, iinc, iload_1, if_icmplt).
spin 方法中的两个常量 0 和 100, 使用两个不同的指令压入操作数栈. 压入 0 使用了 iconst_0 指令, 是 iconst_<i > 指令家族之一. 压入 100 使用了 bipush 指令, 这个指令获取它的立即数压入栈中.
java 虚拟机经常使用操作码隐式的包含操作数(如整型常量 - 1,0,1,2,3,4 和 5 在 iconst_<i > 指令的例子). 因为 iconst_0 指令知道它将要压入一个整数 0,iconst_0 不再需要存储一个操作数来告诉它应该压入哪个值, 也不需要获取和解析一个操作数. 将压入 0 编译成 bipush 0 也是正确的, 凡是会导致 spin 编译后的代码长度增加一个字节. 一个简单的虚拟机也会在每次循环中花费额外的时间来获取和解码显式的操作数. 使用隐含的操作数使得编译后的代码更加紧凑和高效.
spin 方法中的 i 存在 java 虚拟机局部变量 1 中. 因为大多数 java 虚拟机指令操作从操作数栈中弹出的值, 而不是直接使用局部变量, 在为 Java 虚拟机编译的代码中, 在局部变量和操作数堆栈之间传输值的指令很常见. 这些操作同样被指令集特殊的支持. 在 spin 方法中, 值在局部变量表中传输使用 istore_1 和 iload_1 指令, 每个指令都隐式的操作局部变量表中位置为 1 的值. istore_1 指令从操作数栈中弹出一个 int 值, 然后存入局部变量 1 中. iload_1 指令将局部变量 1 的值压入操作数栈.
使用和重用局部变量是编译器作者决定的. 特殊的 load 和 store 指令应该鼓励编译器作者尽可能的重用局部变量. 这样编译后的代码会更快, 更紧凑, 并且使用栈帧更少的空间.
对局部变量的某些非常频繁的操作由 Java 虚拟机专门处理. iinc 指令为局部变量增加一个长度为 1 字节有符号的值. spin 中的 iinc 指令将第一个局部变量 (这个指令的第一个操作数) 加 1(这个指令的第二个操作数).iinc 指令很适合实现循环结构.
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) 提供了中等程度的支持, 仅缺少条件转移指令部分.
java 虚拟机规范(se8)--java 虚拟机的编译(一)
来源: http://www.bubuko.com/infodetail-3076301.html