本文翻译自: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. docker调用yum时“"/usr/libexec/urlgrabber-ext-down" is not installed”

    原因: 1 docker镜像为高版本的fedora30:latest镜像,yum本身已被dnf替代,但部分功能仍不完整: 如:yum-builddep SPECS/xxx.spec 解决办法: 1 安 ...

  2. Ptyhon变量,常量,注释

    变量的命名规则: 1.变量由字母,数字,下划线搭配而成 2.变量不能以数字开头 3.变量也不能是Python的关键字. 4.变量不要有中文 5.名字要有意义 6.名字不要太长 变量的两种命名方式: 1 ...

  3. POJ 2417 Discrete Logging ( Baby step giant step )

    Discrete Logging Time Limit: 5000MS   Memory Limit: 65536K Total Submissions: 3696   Accepted: 1727 ...

  4. top查看进程的参数

    top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器. top显示系统当前的进程和其他状况,是一个动态显示过程,即可以通过用户按键来不 ...

  5. db2备份与恢复

    备份数据库: 离线备份 1.连接至数据库: db2 connect to test user db2admin using db2admin 2.显示数据库应用状态 db2 list applicat ...

  6. redis基础及基本命令

    什么是redis Redis是一个Key-value存储系统,redis提供了丰富的数据结构,包括string(字符串),list(列表),sets(集合),ordered set(有序集合),has ...

  7. 在eclipse中添加svn插件

    1.点击菜单栏中的help选项,然后选择Install New Software,然后点击ADD,输入: name:subclipse     url:http://subclipse.tigris. ...

  8. 对struct typedef *的认识

    typedef struct node { ……… }NODE,*PNODE; 应该等价于 typedef struct node NODE;//struct node = NODE,eg:struc ...

  9. Dictonary(Python)(一)

    基本用法: .keys .values .items >>> D = dict(a=1,b=2,c=3) >>> D {'a': 1, 'b': 2, 'c': 3 ...

  10. Ceph中PG和PGP的区别

    http://www.zphj1987.com/2016/10/19/Ceph%E4%B8%ADPG%E5%92%8CPGP%E7%9A%84%E5%8C%BA%E5%88%AB/ 一.前言 首先来一 ...