本文翻译自: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>以行尾注释语法给出:

   bipush      // Push int constant 

  注释中的一部分是有javap产生的,剩余部分由作者添加的。每条指令前的<index>可以被用于控制转移指令的目标。例如,goto 8这条指令表示跳转到索引为8的指令处执行。需要注意的是,java虚拟机的控制转移指令的实际操作数是当前指令的操作码集合中的地址偏移量,这些操作数会被javap工具按照更容易被人阅读的方式来显示。

  我们在表示运行时常量池索引的操作数的前面加上一个#符号,然后接着指令之后有一条注释来标识引用的运行时常量池项,如下所示:

  ldc #         // Push float constant .

  或者:

   invokevirtual #    // 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
}
}

  编译器可能将其编译为下面的代码:

   iconst_0       // Push int constant
istore_1 // Store into local variable (i=)
goto // 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循环主要由以下指令来实现:

   iinc         // Increment local variable  by  (i++)
iload_1 // Push local variable (i)
bipush // Push int constant
if_icmplt // Compare and loop if less than (i < )

  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()
dconst_0 // Push double constant .
dstore_1 // Store into local variables and
goto // 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)
dload_1 // First argument in local variables and
dload_3 // Second argument in local variables and
dadd
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()
iconst_0
istore_1
goto
iload_1 // The short is treated as though an int
iconst_1
iadd
i2s // Truncate int to short
istore_1
iload_1
bipush
if_icmplt
return

  Java虚拟机中缺少对byte,char和short类型的直接支持并没有大的问题,因为这些类型的值在内部被提升为int(byte和short被符号扩展为int,char是零扩展)。 因此,可以使用int指令对字节,字符和短数据执行操作。 唯一的额外成本是将int操作的值截断为有效范围。

  Java虚拟机对于long和浮点类型(float和double)提供了中等程度的支持,仅缺少条件转移指令部分。

java虚拟机规范(se8)——java虚拟机的编译(一)的更多相关文章

  1. java虚拟机规范(se8)——java虚拟机的编译(四)

    3.12 抛出和处理异常 在程序中使用throw关键字来抛出异常.编译结果很简单. void cantBeZero(int i) throws TestExc { if (i == 0) { thro ...

  2. java虚拟机规范(se8)——java虚拟机结构(一)

    本文翻译自:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html 第二章 虚拟机结构 本文档描述了一个抽象的虚拟机规范,并不描述 ...

  3. java虚拟机规范(se8)——java虚拟机结构(二)

    2.5 运行时数据区域 java虚拟机定义了多个用于程序执行期间的运行时数据区域.这些数据区域中一些随着java虚拟机的启动而创建,随着虚拟机的退出而销毁.其他的数据区域时和线程相关的.线程相关数据区 ...

  4. java虚拟机规范(se8)——java虚拟机结构(六)

    2.11 指令集简介 java虚拟机指令由一个字节的操作码,接着时0个或多个操作数组成,操作码描述了执行的操作,操作数提供了操作所需的参数或者数据.许多指令没有操作数只包含一个操作码. 如果忽略异常处 ...

  5. java虚拟机规范(se8)——java虚拟机的编译(三)

    3.6 接受参数 如果n个参数传给一个实例的方法,按照约定,它们被接受并放在这个新方法创建的栈帧中的局部变量表里,在局部变量表中的序号从1到n.这些参数按照它们传递过来的顺序存放.例如: int ad ...

  6. java虚拟机规范(se8)——java虚拟机的编译(二)

    3.3 算术运算 java虚拟机通常在操作数栈上进行算术运算(例外情况是iinc指令,它直接增加一个局部变量的值).例如下面的align2grain()方法,它的作用是将int值对齐到2的指定次幂: ...

  7. java虚拟机规范(se8)——java虚拟机结构(四)

    2.7 对象的表示 java虚拟机并不要求对象满足任何特定的内部结构. 在Oracle的一些Java虚拟机实现中,对类实例的引用是指向句柄的指针,该句柄本身是一对指针:一个指向包含对象方法的表和指向表 ...

  8. java虚拟机规范(se8)——java虚拟机结构(三)

    2.6. 栈帧 栈帧用于存储数据和部分结果,同样也用于执行动态链接,返回方法的值和分派异常. 当方法被调用的时候会创建一个新的栈帧.当一个方法调用结束时,它对应的栈帧就被销毁了,不管是正常调用结束还是 ...

  9. java虚拟机规范(se8)——java虚拟机结构(五)

    2.10 异常 java虚拟机中的异常用Throwable类或者它的子类的实例来表示.抛出一个异常会导致立即非本地(an inmediate nolocal)的控制转移,从发生异常的地方跳到处理异常的 ...

随机推荐

  1. tomcat开启PID文件

    1.配置tomcat启动后将进程号保存至 ./bin/tomcat.pid 文件. 修改 catalina.sh 文件,在 PRGDIR 下面一行添加 CATALINAPID 参数行,如下: PRGD ...

  2. WPF-将DataGrid控件中的数据导出到Excel

    原文:WPF-将DataGrid控件中的数据导出到Excel 导出至Excel是非常常见,我们可以用很多类库,例如Aspose.NOPI.Interop,在这里我们使用微软自家的工具.我的WPF绑定的 ...

  3. Python二分查找算法

    Python 二分查找算法: 什么是二分查找,二分查找的解释: 二分查找又叫折半查找,二分查找应该属于减值技术的应用,所谓减值法,就是将原问题分成若干个子问题后,利用了规模为n的原问题的解与较小规模( ...

  4. 【学习总结】Python-3-逻辑运算符

    参考:菜鸟教程-Python3运算符 逻辑运算符的计算规则划重点: 并不是只返回布尔型,有时会返回变量的数值 (优先级:not>and>or) 总结: '与或非'三件套中,not与数学逻辑 ...

  5. keepalive+nginx

    1Nginx+keepAlived负载均衡高可用1.1Nginx+keepAlive架构图 1.1.1主机宕机 1.1.2主机恢复 1.1.3高可用环境 两台nginx,一主一备:192.168.10 ...

  6. 十二、结构模式之门面(Facade)模式

    什么是门面模式 门面模式(也有翻译为外观模式)是对象的结构模式,外部与一个子系统的通信必须通过一个统一的门面进行.其为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子 ...

  7. GitHub区域和工作流程

    workspace:工作区 index:暂存区 repository:本地版本库 remote:远程仓库 首先到托管服务器上创建一个空版本库,例如在github.coding.oschina等 然后克 ...

  8. 2019年React学习路线图

    作者|javinpaul 译者|无明 之前我们已经介绍了 2019 年 Vue 学习路线图,而 React 作为当前应用最广泛的前端框架,在 Facebook 的支持下,近年来实现了飞越式的发展,我们 ...

  9. HashMap源码浅析

    HashMap源码主要一些属性 //默认的初始化容量(2的n次方) static final int default_inital_capacity = 16; //最大指定容量为2的30次方 sta ...

  10. ubuntu下oracle 数据库安装

    环境:腾讯云 一. 由于腾讯云直接下载oracle太慢,先安装docker 1.sudo apt update 2.接下来,使用apt安装一些允许通过HTTPS才能使用的软件包: sudo apt i ...