第3章 编译Java虚拟机

Java虚拟机机器旨在支持Java编程语言。Oracle的JDK软件包含一个编译器,从使用Java编程语言编写的源代码到Java虚拟机的指令集,以及一个实现Java虚拟机本身的运行时系统。了解一个编译器如何使用Java虚拟机对于预期的编译器编写器以及尝试理解Java虚拟机本身的编译器编写器非常有用。本章中编号的部分不是规范性的。

注意,当引用从Java虚拟机的指令集到特定CPU的指令集的转换器时,有时使用术语“编译器”。这种转换器的一个示例是即时(JIT)代码生成器,其仅在加载Java虚拟机代码之后生成特定于平台的指令。本章不涉及与代码生成相关的问题,只涉及与使用Java编程语言编写的源代码编译到Java虚拟机指令相关的问题。

3.1。示例格式

本章主要包括源代码示例以及javacOracle JDK版本1.0.2 中的编译器为示例生成的Java虚拟机代码的带注释的列表。Java虚拟机代码是由Oracle javap实用程序输出的非正式“虚拟机汇编语言”编写的,随JDK版本一起发布。您可以使用它javap来生成编译方法的其他示例。

任何读过汇编代码的人都应该熟悉这些示例的格式。每条指令采用以下形式:

<操作码> [ [ ...]] []

是数组中指令的操作码的索引,其中包含此方法的Java虚拟机代码的字节。或者,可以将其视为从方法开始的字节偏移。它是指令操作码的助记符,零或更多是指令的操作数。可选的以行尾注释语法给出:

8    bipush 100      //推入int恒定100

评论中的一些材料是由javap; 其余的由作者提供。每个指令的前缀可以用作控制转移指令的目标。例如,goto 8指令将控制转移到索引8处的指令。注意,Java虚拟机控制转移指令的实际操作数是来自那些指令的操作码的地址的偏移量。这些操作数由javap(并在本章中显示)显示为更容易读取其方法的偏移量。

我们在前面添加一个操作数,表示带有哈希符号的运行时常量池索引,并通过注释来跟踪指令,该注释标识引用的运行时常量池项,如下所示:

10  ldc#1          //推动float常数100.0

要么:

9    invokevirtual#4    //方法Example.addTwo(II)I

出于本章的目的,我们不担心指定操作数大小等细节。

3.2。使用常量,局部变量和控制结构

Java虚拟机代码展示了Java虚拟机设计和使用类型所强加的一组通用特性。在第一个例子中,我们遇到了很多这些,我们会详细考虑它们。

该spin 方法只需旋转一个空的for循环100次:

void spin(){    int i;    for(i = 0; i <100; i ++){        ; //循环体是空的    }}

编译器可能编译spin为:

0    iconst_0        //推送int常量01    istore_1        //存储到局部变量1(i = 0)2    转到8          //第一次通过不增加5    iinc 1 1        //将局部变量1递增1(i ++)8    iload_1        //推送局部变量1(i)9    bipush 100      //推动int常数10011  if_icmplt 5    //如果小于(i <100)则比较并循环14  return          //完成后返回void

Java虚拟机是面向堆栈的,大多数操作从Java虚拟机当前帧的操作数堆栈中获取一个或多个操作数,或将结果推回操作数堆栈。每次调用一个方法时都会创建一个新帧,并在其中创建一个新的操作数堆栈和一组局部变量供该方法使用(第2.6节)。在计算的任何一点,因此每个控制线程可能存在许多帧和相同数量的操作数堆栈,对应于许多嵌套方法调用。只有当前帧中的操作数堆栈处于活动状态。

Java虚拟机的指令集通过使用不同的字节码对其各种数据类型进行操作来区分操作数类型。该方法spin仅对类型值起作用 int。选择用于对类型化数据(iconst_0istore_1iinciload_1if_icmplt)进行操作的编译代码中的指令都是专用于类型的int。

两个常量spin,0 以及100,被压入到使用两种不同的指令的操作数堆栈。在0使用推iconst_0指令,家庭中的一个的iconst_ 的指令。在100使用推bipush 指令,该指令获取它推动作为立即操作数的值。

Java虚拟机经常需要一定的操作数(可能性的优点int 常数-1012345中所述的情况下 iconst_的通过使那些操作数在操作码的隐式指示)。由于iconst_0指令知道它要推的int 0,iconst_0不需要存储操作数来告诉它要推送什么值,也不需要获取或解码操作数。编译推送0为 bipush 0本来是正确的,但是会使编译后的代码spin长一个字节。一个简单的虚拟机每次在循环周围也会花费额外的时间来获取和解码显式操作数。隐式操作数的使用使得编译代码更加紧凑和高效。

将int i 在spin存储为Java虚拟机的局部变量 1。由于大多数Java虚拟机指令对从操作数堆栈弹出的值而不是直接对局部变量进行操作,因此在为Java虚拟机编译的代码中,在局部变量和操作数堆栈之间传输值的指令很常见。这些操作在指令集中也有特殊支持。在spin,使用istore_1iload_1指令将值传入传出局部变量,每个指令都隐式地对局部变量1进行操作。所述istore_1指令弹出一个 int从操作数堆栈,并将其存储在局部变量1。所述iload_1指令将在本地变量中的值1到操作数栈。

局部变量的使用(和重用)是编译器编写者的责任。专门的加载和存储指令应该鼓励编译器编写器尽可能多地重用局部变量。生成的代码更快,更紧凑,并且在帧中占用更少的空间。

某些对局部变量的非常频繁的操作是由Java虚拟机专门提供的。该iinc指令由一个字节的符号值递增一个局部变量的内容。所述iinc在指令spin增量第一本地变量(其第一操作数)由1(其第二操作数)。实现循环结构时, iinc指令非常方便。

在for 中环spin是由这些指令主要完成:

5    iinc 1 1        //将局部变量1递增1(i ++)8    iload_1        //推送局部变量1(i)9    bipush 100      //推动int常数10011  if_icmplt 5    //如果小于(i <100)则比较并循环

所述bipush指令将值100到操作数栈作为 int,则if_icmplt指令弹出关闭操作数堆栈该值,并确定它。如果比较成功(变量i小于100),则控制转移到索引5并for开始循环的下一次迭代。否则,控制传递给if_icmplt之后的 指令

如果spin 示例使用的数据类型不是int循环计数器,则编译的代码必须更改以反映不同的数据类型。例如,如果代替的int 的spin例子使用一个double,如图所示:

void dspin(){    双倍;    for(i = 0.0; i <100.0; i ++){        ; //循环体是空的    }}

编译后的代码是:

方法void dspin()0    dconst_0        //推双倍常数0.01    dstore_1        //存储到局部变量1和2中2    转到9          //第一次通过不增加5    dload_1        //推送局部变量1和26    dconst_1        //推双倍常数1.07    dadd            //添加; 没有dinc指令8    dstore_1        //将结果存储在局部变量1和2中9    dload_1        //推送局部变量1和210  ldc2_w#4      //按下双常数100.013  dcmpg          //没有if_dcmplt指令14  iflt 5          //如果小于(i <100.0)则比较并循环17  return          //完成后返回void

对类型数据进行操作的指令现在专用于类型double。(ldc2_w 指令将在本章后面讨论。)

回想一下,double值只占用两个局部变量,尽管它们只能使用两个局部变量的较小索引来访问。对于类型的值也是如此long。再举个例子,

double doubleLocals(double d1,double d2){    返回d1 + d2;}

方法double doubleLocals(double,double)0    dload_1        //局部变量1和2中的第一个参数1    dload_3        //局部变量3和4中的第二个参数2    DADD

3    dreturn

请注意,用于存储double值的局部变量对的局部变量doubleLocals绝不能单独操作。

Java虚拟机的操作码大小为1个字节,导致其编译的代码非常紧凑。但是,1字节操作码也意味着Java虚拟机指令集必须保持较小。作为折衷方案,Java虚拟机不能为所有数据类型提供相同的支持:它不是完全正交的(表2.11.1-A)。

例如,类型的值的比较int在for实施例的语句spin可以使用单个实现 if_icmplt指令; 但是,Java虚拟机指令集中没有单一指令对类型值执行条件分支double。因此,dspin必须实现它的类型的值的比较double使用dcmpg指令后跟IFLT指令。

Java虚拟机为类型数据提供最直接的支持int。这部分是因为期望Java虚拟机的操作数堆栈和局部变量数组的有效实现。它还受到int典型程序中数据频率的推动。其他整体类型的直接支持较少。例如,没有byte,char或short版本的商店,加载或添加指令。以下是spin使用以下代码编写的示例short:

void sspin(){    我;    for(i = 0; i <100; i ++){        ; //循环体是空的    }}

必须编译为Java虚拟机,如下所示,使用另一种类型的操作,最有可能的指示int,之间的转换short和int值必须确保在操作的结果short数据停留在适当的范围内:

方法void sspin()0    iconst_0

1    istore_1

2    goto 10

5    iload_1        //将短片视为int6    iconst_1

7    iadd

8    i2s            //将int截断为short9    istore_1

10  iload_1

11  bipush 100

13  if_icmplt 5

16  return

由于缺乏直接的支持 byte,char和short类型的Java虚拟机是不是特别痛苦,因为这些类型的值在内部晋升 int(byte和short被符号扩展为int,char是零扩展)。操作上byte,char和short数据因此可以使用进行int说明。唯一的额外成本是将int操作值截断为有效范围。

在long和浮点类型有支持的Java虚拟机的中间水平,仅缺少的条件控制转移指令完全互补。

3.3。算术

Java虚拟机通常对其操作数堆栈进行算术运算。(例外是iinc 指令,它直接递增局部变量的值。)例如,该align2grain方法将int值与给定的2的幂对齐 :

int align2grain(int i,int grain){    返回((i + grain-1)&〜(grain-1));}

算术运算的操作数从操作数堆栈中弹出,操作结果被推回操作数堆栈。因此,算术子计算的结果可以作为其嵌套计算的操作数。例如,计算~(grain-1)由这些指令处理:

5    iload_2        //推谷物6    iconst_1        //推送int常量17    isub            //减去; 推送结果8    iconst_m1      //推送int常量-19    ixor            //做XOR; 推送结果

首先grain-1使用局部变量2的内容和立即int值计算1。这些操作数从操作数堆栈中弹出,它们的差异被推回到操作数堆栈上。因此,差异立即可用作ixor指令的一个操作数。(回想一下~x == -1^x。)类似地,ixor指令的结果成为后续iand指令的操作数。

整个方法的代码如下:

方法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,在使用的托管LDCldc_wldc2_w 说明。

LDCldc_w 指令用于访问在运行时间常量池(包括类的实例值String)的类型比其它double 和long。该ldc_w指令代替使用最不发达国家时,才会有大量的运行时间常量池项,并需要更大的索引访问的项目。所述ldc2_w指令用于访问的类型的所有值double和long; 没有非广泛的变体。

的类型的积分常数 byte,char或者short,还有小int的值,可使用被编译bipushsipush,或iconst_ 指令(§3.2)。可以使用fconst_ dconst_ 指令编译某些小的浮点常量。

在所有这些情况下,编译都很简单。例如,常量为:

void useManyNumeric(){    int i = 100;    int j = 1000000;    long l1 = 1;    long l2 = 0xffffffff;    双d = 2.2;    ......做一些计算......}

设置如下:

方法void useManyNumeric()0    bipush 100    //使用bipush推动小int常数2    istore_1

3    ldc#1        //用ldc推送大的int常量(1000000)5    istore_2

6    lconst_1      //一个很小的长值使用小的快速lconst_17    lstore_3

8    ldc2_w#6    //推长0xffffffff(即int -1)        //可以使用ldc2_w推送任何长常量值11  lstore 5

13  ldc2_w#8    //推双常数2.200000        //也会使用ldc2_w推送不常见的double值16  dstore 7......做那些计算......

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){        我++;    }}

编译为:

方法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){        我++;    }}

编译为:

方法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      //推双倍常数100.113  dcmpg          //要比较和分支,我们必须使用...14  iflt 5          // ...两条指令17  回归

每个浮点类型有两个比较指令:fcmplfcmpg类型float,和 dcmpldcmpg类型double。变体的不同之处仅在于它们对NaN的处理。NaN是无序的(§2.3.2),因此如果它们的任何一个操作数是NaN,则所有浮点比较都会失败。无论比较在非NaN值上失败还是遇到NaN,编译器都会选择产生相同结果的相应类型的比较指令的变体。例如:

int lessThan100(double d){    if(d <100.0){        返回1;     } else {        返回-1;     }}

编译为:

方法int lessThan100(double)0    dload_1

1    ldc2_w#4      //按下双精度 100.04    dcmpg          //如果d为NaN或d> 100.0,则按 1;                  //如果d == 100.0,则按05    ifge 10        //分支0或18    iconst_1

9    ireturn

10  iconst_m1

11  ireturn

如果d不是NaN且小于100.0,则dcmpg指令将int -1压入操作数堆栈,并且ifge指令不分支。无论d 是大于100.0还是NaN,dcmpg 指令将int 1推入操作数堆栈,ifge分支。如果d等于 100.0,则dcmpg指令将int 0推 到操作数堆栈上,并且ifge 分支。

如果比较相反,dcmpl指令可以实现相同的效果:

int greaterThan100(double d){    if(d> 100.0){        返回1;     } else {        返回-1;     }}

变为:

方法int greaterThan100(double)0    dload_1

1    ldc2_w#4      //按下双精度 100.04    dcmpl          //如果d为NaN或d <100.0,则按 -1                  //如果d == 100.0,则按05    ifle 10        //分支为0或-18    iconst_1

9    ireturn

10  iconst_m1

11  ireturn

再次,无论比较是否在非NaN值上失败,还是因为传递了NaN, dcmpl指令都会将int值推送到操作数堆栈上,导致ifle分支。如果两个dcmp 指令都不存在,那么其中一个示例方法将不得不做更多的工作来检测NaN。

3.6。接收参数

如果将n个参数传递给实例方法,则按照惯例,它们将在为新方法调用创建的帧的编号为1n的局部变量中接收。参数按照它们传递的顺序接收。例如:

int addTwo(int i,int j){    返回i + j;}

编译为:

方法int addTwo(int,int)0    iload_1        //推送局部变量1的值(i)1    iload_2        //推送局部变量2(j)的值2    iadd            //添加; 将int结果保留在操作数堆栈上3    ireturn        //返回int结果

按照惯例,实例方法reference在本地变量0中传递给它的实例。在Java编程语言中,可以通过this关键字访问实例。

Class(static)方法没有实例,因此对于它们来说,不需要使用局部变量0。类方法开始在索引0处使用局部变量。如果addTwo方法是类方法,则其参数将以与第一个版本类似的方式传递:

static int addTwoStatic(int i,int j){    返回i + j;}

编译为:

方法int addTwoStatic(int,int)0    iload_0

1    iload_1

2    iadd

3    ireturn

唯一的区别是方法参数出现在局部变量0而不是1

3.7。调用方法

实例方法的常规方法调用将调度对象的运行时类型。(它们是虚拟的,用C ++术语。)这样的调用是使用invokevirtual指令实现的,该指令将运算时常量池条目的索引作为其参数,给出对象类类型的二进制名称的内部形式。 ,要调用的方法的名称,以及该方法的描述符(第4.3.3节)。要调用addTwo之前定义为实例方法的方法,我们可能会写:

int add12and13(){    return addTwo(12,13);}

这编译为:

方法int add12and13()0    aload_0              //推送局部变量0(this)1    bipush 12            //推动int常数123    bipush 13            //推动int常数135    invokevirtual#4    //方法Example.addtwo(II)I8    ireturn              //在操作数堆栈顶部返回int;                        //它是addTwo()的int结果

通过首先将a推reference送到当前实例this,然后到操作数堆栈来设置调用。然后推送方法调用的参数,int值12和 13。addTwo创建方法的框架时,传递给方法的参数将成为新框架的局部变量的初始值。即,reference对于this与两个参数,压入操作数堆栈被调用,将成为局部变量的初始值01,和2被调用的方法的。

最后,addTwo被调用。当它返回时,它的int返回值被推送到调用者的框架的操作数堆栈,即add12and13方法。因此返回值就会立即返回给调用者 add12and13。

从返回add12and13由处理ireturn 的指令add12and13。所述ireturn 指令获取int由返回的值addTwo,对当前帧的操作数堆栈上,并将其推到调用者的帧的操作数堆栈。然后它将控制返回给调用者,使调用者的帧最新。Java虚拟机提供了许多数字和不同的返回指令reference的数据类型,以及一个返回 没有返回值的方法的指令。同一组返回指令用于所有类型的方法调用。

invokevirtual指令的操作数 (在该示例中,运行时常量池索引#4)不是类实例中方法的偏移量。编译器不知道类实例的内部布局。相反,它生成对实例方法的符号引用,这些引用存储在运行时常量池中。在运行时解析这些运行时常量池项以确定实际的方法位置。对于访问类实例的所有其他Java虚拟机指令也是如此。

调用 addTwoStatic,类的(static)变体addTwo类似,如下所示:

int add12and13(){    return addTwoStatic(12,13);}

尽管使用了不同的Java虚拟机方法调用指令:

方法int add12and13()0    bipush 12

2    bipush 13

4    invokestatic#3      //方法示例.addTwoStatic(II)I7    ireturn

编译class(static)方法的调用非常类似于编译实例方法的调用,除非调用者没有传递。因此,从局部变量0(§3.6)开始接收方法参数。该 invokestatic指令总是被用来调用类的方法。

invokespecial 指令必须被用来调用实例初始化方法(第3.8节)。在调用superclass(super)中的private方法和调用方法时也使用它。例如,给定类Near 并Far声明为:

类近{    在它;    public int getItNear(){        return getIt();    }    private int getIt(){        把它返还;    }}远远延伸到{    int getItFar(){        return super.getItNear();    }}

方法Near.getItNear(调用private 方法)变为:

方法int getItNear()0    aload_0

1    invokespecial#5    //方法Near.getIt()I4    ireturn

该方法Far.getItFar(调用超类方法)变为:

方法int getItFar()0    aload_0

1    invokespecial#4    //方法Near.getItNear()I4    ireturn

请注意,使用invokespecial指令this调用的方法始终作为其第一个参数传递给调用的方法。像往常一样,它在局部变量0中被接收。

要调用方法句柄的目标,编译器必须形成一个记录实际参数和返回类型的方法描述符。编译器可能不会对参数执行方法调用转换; 相反,它必须根据自己未转换的类型将它们推入堆栈。reference像往常一样,编译器安排方法句柄对象在参数之前被压入堆栈。编译器发出invokevirtual指令,该 指令引用描述参数和返回类型的描述符。通过与方法解析(特别安排§5.4.3.3),一个invokevirtual如果方法描述符在语法上格式良好并且可以解析描述符中指定的类型 ,则调用invokeExact或者invoke方法的 指令java.lang.invoke.MethodHandle将始终链接。

3.8。使用类实例

Java虚拟机类实例是使用Java虚拟机的指令创建的。回想一下,在Java虚拟机的层面上,构造函数显示为具有编译器提供的名称的方法。这种特殊命名的方法称为实例初始化方法(§2.9)。对于给定的类,可以存在与多个构造函数对应的多个实例初始化方法。一旦创建了类实例并且其实例变量(包括该类及其所有超类的实例变量)已初始化为其默认值,就会调用新类实例的实例初始化方法。例如:

Object create(){    return new Object();}

编译为:

方法java.lang.Object create()0    new#1              //类java.lang.Object3    dup

4    invokespecial#4    //方法java.lang.Object。()V7    转过来

类实例的传递和返回(作为reference类型)非常类似于数值,尽管类型 reference有自己的指令补充,例如:

int i; //实例变量MyObj示例(){    MyObj o = new MyObj();    返回傻(o);}MyObj傻(MyObj o){    if(o!= null){        返回o;    } else {        返回o;    }}

变为:

方法MyObj示例()0    new#2              // Class MyObj3    dup

4    invokespecial#5    //方法MyObj。()V7    astore_1

8    aload_0

9    aload_1

10  invokevirtual #4    //方法Example.silly(LMyObj;)LMyObj;13  回合方法MyObj傻(MyObj)0    aload_1

1    ifnull 6

4    aload_1

5    areturn

6    aload_1

7    areturn

使用getfieldputfield 指令访问类实例(实例变量)的字段。如果i是类型的实例变量 int,则方法setIt 和getIt定义为:

void setIt(int value){    i =价值;}int getIt(){    回归我;}

成为:

方法void setIt(int)0    aload_0

1    iload_1

2    putfield#4    // Field Example.i I5    回归方法int getIt()0    aload_0

1    getfield#4    // Field Example.i I4    ireturn

与方法调用指令的操作数一样,putfieldgetfield 指令的操作数(运行时常量池索引#4)不是类实例中字段的偏移量。编译器生成对实例字段的符号引用,这些引用存储在运行时常量池中。在运行时解析这些运行时常量池项以确定引用对象中字段的位置。

3.9。数组

Java虚拟机阵列也是对象。使用一组不同的指令创建和操作数组。所述newarray指令用于创建数字类型的阵列。代码:

void createBuffer(){    int buffer [];    int bufsz = 100;    int值= 12;    buffer = new int [bufsz];    buffer [10] = value;    value = buffer [11];}

可能编译为:

方法void createBuffer()0    bipush 100      //推动int常数100(bufsz)2    istore_2        //将bufsz存储在局部变量2中3    bipush 12      //推动int常数12(值)5    istore_3        //将值存储在局部变量3中6    iload_2        //推送bufsz ......7    newarray int    // ...并创建该长度的新int数组9    astore_1        //将新数组存储在缓冲区中10  aload_1        //推送缓冲区11  bipush 10      //推动int常数1013  iload_3        //推送值14  iastore        //在缓冲区存储值[10]15  aload_1        //推送缓冲区16  bipush 11      //推入int常数1118  iaload          //在缓冲区推送值[11] ......19  istore_3        // ...并将其存储在值中20  回归

所述anewarray指令用于创建对象的引用的一维阵列,例如:

void createThreadArray(){    线程线程[];    int count = 10;    threads = new Thread [count];    threads [0] = new Thread();}

变为:

方法void createThreadArray()0    bipush 10            //推动int常数102    istore_2            //初始化计数3    iload_2              //推送计数,由anewarray使用4    anewarray类#1  //创建一个新的Thread类数组7    astore_1            //在线程中存储新数组8    aload_1              //推送线程值9    iconst_0            //推送int常量010  new#1              //创建类Thread的实例13  dup                  //重复参考...14      为Thread的构造函数调用特殊的#5 // ...                        //方法java.lang.Thread。()V17  aastore              //将新的Thread存储在0的数组中18  回归

所述anewarray指令也可以被用来创建一个多维阵列的第一维度。或者,multianewarray指令可用于一次创建多个维度。例如,三维数组:

int [] [] [] create3DArray(){    int grid [] [] [];    grid = new int [10] [5] [];    返回网格;}

由以下人员创建:

方法int create3DArray()[] [] []0    bipush 10                //推入int 10(尺寸1)2    iconst_5                  //推送int 5(维度二)3    multianewarray#1 dim#2 //类[[[我,一个三维的                            // int数组; 只创造了                            //前两个维度7    astore_1                  //存储新阵列......8    aload_1                  // ...然后准备归还它9    转过来

multianewarray指令的第一个操作数 是要创建的数组类类型的运行时常量池索引。第二个是实际创建的数组类型的维数。所述multianewarray 指令可以被用来创建类型的所有尺寸,对于码create3DArray节目。请注意,多维数组只是一个对象,因此分别由aload_1areturn指令加载和返回。有关数组类名称的信息,请参见 §4.4.1。

所有数组都有相关的长度,可通过arraylength指令访问。

3.10。编译开关

switch 语句汇编使用tableswitchlookupswitch指令。所述 tableswitch时的情况下指令用于switch能够有效地表示为指数为目标偏移的表。如果表达式的值落在有效索引范围之外default,switch则使用该目标switch。例如:

int chooseNear(int i){    开关(i){        案例0:返回0;        案例1:返回1;        案例2:返回2;        默认值:return -1;    }}

编译为:

方法int chooseNear(int)0    iload_1              //推送局部变量1(参数i)1个    tableswitch 0至2: //有效索引是0至2

      0:28              //如果i是0,继续在28

      1:30              //如果i为1,继续在30

      2:32              //如果i是2,继续32

      默认值:34        //否则,继续3428  iconst_0            //我是0; push int constant 0 ...29  ireturn              // ...然后归还它30  iconst_1            //我是1; push int constant 1 ...31  ireturn              // ...然后归还它32  iconst_2            //我是2; push int constant 2 ...33  ireturn              // ...然后归还它34  iconst_m1            //否则按int int -1 ...35  ireturn              // ...然后归还它

Java虚拟机的tableswitchlookupswitch指令仅对int数据有效。因为对on byte,char或或short值的操作在内部被提升为int,所以对其switch表达式求值为其中一种类型进行编译,就好像它被评估为类型一样int。如果 chooseNear方法是使用type编写的,则使用类型时short将生成相同的Java虚拟机指令int。必须缩小其他数字类型以键入int 以在a中使用switch。

在switch稀疏的情况下, tableswitch 指令的表表示在空间方面变得低效。所述lookupswitch 指令可以替代地使用。所述lookupswitch指令对 int键(所述的值case的标签)与表目标偏移。当执行lookupswitch指令时,表达式的值switch与表中的键进行比较。如果其中一个键与表达式的值匹配,则在关联的目标偏移处继续执行。如果没有键匹配,则在default目标处继续执行。例如,编译代码:

int chooseFar(int i){    开关(i){        case -100:return -1;        案例0:返回0;        案例100:返回1;        默认值:return -1;    }}

chooseNear除了lookupswitch 指令外,它看起来就像代码一样 :

方法int chooseFar(int)0    iload_1

1    lookupswitch 3:-100:36 0:38 100:40 默认:42

36  iconst_m1

37  ireturn

38  iconst_0

39  ireturn

40  iconst_1

41  ireturn

42  iconst_m1

43  ireturn

Java虚拟机指定lookupswitch指令的表必须按键排序,以便实现可以使用比线性扫描更高效的搜索。即便如此,lookupswitch指令必须搜索其键以进行匹配,而不是简单地执行边界检查并索引到像tableswitch这样的。因此,tablewitch指令可能比空间考虑允许选择的lookupswitch更有效。

3.11。操作数堆栈上的操作

Java虚拟机具有大量指令,这些指令将操作数堆栈的内容作为无类型值进行操作。这些是有用的,因为Java虚拟机依赖于对其操作数堆栈的灵巧操作。例如:

public long nextIndex(){    return index ++;}私人长指数= 0;

编译为:

方法long nextIndex()0    aload_0        //推这个1    dup            //复制一份2    getfield#4    //消耗了其中一个副本                  //推长场指数,                  //高于原来的这个5    dup2_x1        //操作数堆栈顶部的长整数                  //插入到下面的操作数堆栈中                    //原来这个6    lconst_1        //推长常数17    ladd            //索引值递增...8    putfield#4    // ...并将结果存储在字段中11  lreturn        //索引的原始值在顶部                  //操作数堆栈,准备好返回

请注意,Java虚拟机从不允许其操作数堆栈操作指令修改或拆分操作数堆栈上的各个值。

3.12。投掷和处理例外情况

使用throw关键字的程序会抛出异常。它的编译很简单:

void cantBeZero(int i)抛出TestExc {    if(i == 0){        抛出新的TestExc();    }}

变为:

方法void cantBeZero(int)0    iload_1              //推送参数1(i)1    ifne 12              //如果i == 0,则分配实例并抛出4    new#1              //创建TestExc实例7    dup                  //一个引用转到它的构造函数8    invokespecial#7    // Method TestExc。()V11  athrow              //抛出第二个参考12  返回              //如果我们抛出TestExc,永远不要到这里

编译try- catch 结构很简单。例如:

void catchOne(){    尝试{        试试看();    } catch(TestExc e){        handleExc(E);    }}

编译为:

方法void catchOne()0    aload_0              //尝试块的开始1    invokevirtual#6    //方法Example.tryItOut()V4    return              // try块结束; 正常回报5    astore_1            //将抛出的值存储在本地var 1中6    aload_0              //推这个7    aload_1              //推送抛出的值8    invokevirtual#5    //调用处理程序方法:                        // Example.handleExc(LTestExc;)V11  return              //处理TestExc后返回例外表:从目标类型0 4 5 Class TestExc

仔细观察, try块的编译方式与try不存在时一样:

方法void catchOne()0    aload_0              //尝试块的开始1    invokevirtual#6    //方法Example.tryItOut()V4    return              // try块结束; 正常回报

如果在执行try块期间没有抛出异常,则表现得好像 try不存在:tryItOut调用并catchOne返回。

继try块是实现单一的Java虚拟机代码的catch条款:

5    astore_1            //将抛出的值存储在本地var 1中6    aload_0              //推这个7    aload_1              //推送抛出的值8    invokevirtual#5    //调用处理程序方法:                        // Example.handleExc(LTestExc;)V11  return              //处理TestExc后返回例外表:从目标类型0 4 5 Class TestExc

调用子句handleExc的内容catch也像普通方法调用一样编译。但是,catch子句的存在会导致编译器生成异常表条目(§2.10, §4.7.3)。该catchOne方法的异常表有一个条目对应于可以处理TestExc的catch子句的一个参数(类的实例)catchOne。如果TestExc在索引04之间执行指令期间抛出某个值为实例的值 在catchOne,控制转移到索引5处的Java虚拟机代码,该代码实现catch子句的块。如果抛出的值不是实例 TestExc,则该catch子句catchOne无法处理它。相反,该值被重新抛出给调用者catchOne。

A try可能有多个 catch子句:

void catchTwo(){    尝试{        试试看();    } catch(TestExc1 e){        handleExc(E);    } catch(TestExc2 e){        handleExc(E);    }}

通过简单地为每个子句一个接一个地附加Java虚拟机代码并将条目添加到异常表中来编译catch给定try语句的多个子句catch,如下所示:

方法void catchTwo()0    aload_0              //开始尝试阻止1    invokevirtual#5    //方法Example.tryItOut()V4    return              // try块结束; 正常回报5    astore_1            // TestExc1的处理程序的开头;                        //将抛出的值存储在本地var 1中6    aload_0              //推这个7    aload_1              //推送抛出的值8    invokevirtual#7    //调用处理程序方法:                        // Example.handleExc(LTestExc1;)V11  return              //处理TestExc1后返回12  astore_1            // TestExc2的处理程序的开头;                        //将抛出的值存储在本地var 1中13  aload_0              //推这个14  aload_1              //推送抛出的值15  invokevirtual#7    //调用处理程序方法:                        // Example.handleExc(LTestExc2;)V18  返回              //处理TestExc2后返回例外表:从目标类型0 4 5 Class TestExc10 4 12 Class TestExc2

如果在执行try子句期间(索引04之间)抛出一个与一个或多个catch子句的参数匹配的值(该值是一个或多个参数的实例),则第一个(最里面的)这样的 catch子句被选中。控制转移到该catch子句块的Java虚拟机代码。如果抛出的值与任何catch子句的参数不匹配catchTwo,则Java虚拟机将重新抛出该值而不调用任何子catch句中的代码catchTwo。

嵌套try- catch 语句的编译非常类似于try带有多个catch子句的语句 :

void nestedCatch(){    尝试{        尝试{            试试看();        } catch(TestExc1 e){            handleExc1(E);        }    } catch(TestExc2 e){        handleExc2(E);    }}

变为:

方法void nestedCatch()0    aload_0              //开始尝试阻止1    invokevirtual#8    //方法Example.tryItOut()V4    return              // try块结束; 正常回报5    astore_1            // TestExc1的处理程序的开头;                        //将抛出的值存储在本地var 1中6    aload_0              //推这个7    aload_1              //推送抛出的值8    invokevirtual#7    //调用处理程序方法:                        // Example.handleExc1(LTestExc1;)V11  return              //处理TestExc1后返回12  astore_1            // TestExc2的处理程序的开头;                        //将抛出的值存储在本地var 1中13  aload_0              //推这个14  aload_1              //推送抛出的值15  invokevirtual#6    //调用处理程序方法:                        // Example.handleExc2(LTestExc2;)V18  返回              //处理TestExc2后返回例外表:从目标类型0 4 5 Class TestExc10 12 12 Class TestExc2

catch 子句的嵌套仅在异常表中表示。Java虚拟机不强制执行异常表条目的嵌套或任何排序(第2.10节)。但是,因为try- catch 构造是结构化的,所以编译器总是可以对异常处理程序表的条目进行排序,以便对于该方法中的任何抛出异常和任何程序计数器值,匹配抛出异常的第一个异常处理程序对应于最内部匹配catch条款。

例如,如果tryItOut(在索引1处)的调用抛出了一个实例TestExc1,则它将由catch 调用的子句处理handleExc1。即使异常发生在外部catch 子句(捕获TestExc2)的边界内,即使该外部catch子句可能已经能够处理抛出的值,也是如此。

作为一个微妙的点,请注意一个catch子句的范围包含在“从”端,而在“到”端是排他性的(第4.7.3节)。因此,catch子句catch的异常表条目TestExc1不包括 偏移量4处的返回指令。但是,子句catch的异常表条目确实覆盖了 偏移量11处的返回指令。嵌套子句中的返回指令包含在嵌套子句所涵盖的指令范围内。 catchTestExc2catchcatch

3.13。编译finally

(本节假设编译器生成class版本号为50.0或更低的文件,以便可以使用jsr指令。另请参见 §4.10.2.5。)

一个汇编 try- finally声明是类似的try- catch。在将控制转移到try语句之外之前,无论该转移是正常还是突然,因为抛出了异常,finally必须首先执行该子句。对于这个简单的例子:

void tryFinally(){    尝试{        试试看();    } finally {        包起来();    }}

编译后的代码是:

方法void tryFinally()0    aload_0              //尝试块的开始1    invokevirtual#6    //方法Example.tryItOut()V4    jsr 14              //最后调用阻止7    返回              //尝试块结束8    astore_1            //任何投掷的处理程序的开始9    jsr 14              //最后调用阻止12  aload_1              //推送抛出的值13  athrow              // ...并向调用者重新抛出值14  astore_2            //终止块的开始15  aload_0              //推这个16  invokevirtual#5    //方法Example.wrapItUp()V19  ret 2                //从最后一块回来例外表:从目标类型0 4 8任何

有四种控制方式可以在try语句之外传递:通过返回该块的底部,返回,执行break或 continue声明,或者引发异常。如果tryItOut返回而没有引发异常,则finally使用jsr 指令将控制转移到块。的JSR 14在索引指令4为“子程序调用”为代码finally在索引块14(该finally块被编译为一个嵌入的子程序)。当finally块完成时,ret 2指令将控制返回到索引 4处的 jsr指令之后的指令。

更详细地说,子例程调用的工作原理如下:jsr指令在跳转之前将下一条指令的地址(在索引7返回)推送到操作数堆栈上。作为跳转目标的astore_2指令将操作数堆栈上的地址存储到本地变量2中。用于代码块(在这种情况下,aload_0invokevirtual指令)时运行。假设执行该代码正常完成,则retfinally 指令从局部变量2中检索地址并继续在该地址执行。该返回指令被执行,tryFinally正常返回。

一个try与语句 finally子句编写有一个特殊的例外处理程序,一个可以处理中抛出的任何异常try声明。如果 tryItOut抛出异常,则在异常表中tryFinally搜索适当的异常处理程序。找到特殊处理程序,导致执行在索引8处继续。索引8处的astore_1指令将抛出的值存储到局部变量1中。以下jsr指令执行子程序调用代码finally块。假设代码正常返回,索引12处的aload_1指令将抛出的值推回操作数堆栈,并且后面的athrow指令重新抛出该值。

try使用catch子句和finally子句编译语句更复杂:

void tryCatchFinally(){    尝试{        试试看();    } catch(TestExc e){        handleExc(E);    } finally {        包起来();    }}

变为:

方法void tryCatchFinally()0    aload_0              //尝试块的开始1    invokevirtual#4    //方法Example.tryItOut()V4    转到16              //跳到最后一个块7    astore_3            // TestExc的处理程序的开头;                        //将抛出的值存储在本地var 3中8    aload_0              //推这个9    aload_3              //推送抛出的值10  invokevirtual#6    //调用处理程序方法:                        // Example.handleExc(LTestExc;)V13  goto 16              //这个goto是不必要的,但是                        //由javac在JDK 1.0.2中生成16  jsr 26              //最后调用阻止19  返回              //处理TestExc后返回20  astore_1            //异常处理程序的开头                        //除了TestExc或异常                        //在处理TestExc时抛出21  jsr 26              //终于呼叫阻止24  aload_1              //推送抛出的价值......25  athrow              // ...并向调用者重新抛出值26  astore_2            //终止块的开始27  aload_0              //推这个28  invokevirtual#5    //方法Example.wrapItUp()V31  ret 2                //从最后一个块返回例外表:从目标类型0 4 7 Class TestExc0 16 20任何

如果try语句正常完成,索引4处的goto指令将跳转到索引为16的块的子例程调用 。执行索引26处的块,控制返回索引19处的 返回指令,并正常返回。 finallyfinallytryCatchFinally

如果tryItOut抛出一个实例TestExc,则选择异常表中第一个(最里面的)适用的异常处理程序来处理异常。从索引7开始,该异常处理程序的代码将抛出的值传递给handleExc其返回并在其返回时对finally索引26处的块进行 相同的子例程调用,如同正常情况一样。如果没有抛出异常handleExc,则 tryCatchFinally返回正常。

如果tryItOut抛出的值不是实例TestExc或者handleExc自身抛出异常,则该条件由异常表中的第二个条目处理,该条件处理索引016之间抛出的任何值。该异常处理程序将控制转移到索引20,其中抛出的值首先存储在局部变量1中。finally 索引26处的块的代码称为子例程。如果它返回,则从局部变量1中检索抛出的值,并使用该值重新抛出远程 教学。如果在执行finally子句期间抛出新值,则该finally子句将中止,并tryCatchFinally突然返回,将新值抛给 其调用者。

3.14。同步

Java虚拟机中的同步由监视器进入和退出实现,显式地(通过使用monitorentermonitorexit指令)或隐式地(通过方法调用和返回指令)。

对于用Java编程语言编写的代码,最常见的同步形式可能就是 synchronized方法。甲synchronized方法通常不使用实施monitorentermonitorexit。相反,它只是通过ACC_SYNCHRONIZED标志在运行时常量池中进行区分,该 标志由方法调用指令(第2.11.10节)检查。

monitorentermonitorexit指令使得汇编synchronized 语句。例如:

void onlyMe(Foo f){    synchronized(f){        做一点事();    }}

编译为:

方法void onlyMe(Foo)0    aload_1              //按f1    dup                  //在堆栈上复制它2    astore_2            //在本地变量2中存储副本3    monitorenter        //输入与f关联的监视器4    aload_0              //拿着显示器,传递这个......5    invokevirtual#5    // ...调用Example.doSomething()V8    aload_2              //推送局部变量2(f)9    monitorexit          //退出与f关联的监视器10  转到18              //正常完成方法13  astore_3            //如果有任何投掷,最终到此为止14  aload_2              //推送局部变量2(f)15  monitorexit          //一定要退出显示器!16  aload_3              //推送抛出的值...17  athrow              // ...并向调用者重新抛出值18  返回              //在正常情况下返回例外表:从目标类型4 10 13任何13 16 13任何

编译器可以确保在任何方法调用完成时,monitorexit指令将已经针对每个被执行monitorenter由于方法调用执行的指令。无论方法调用是正常完成(第2.6.4节)还是突然完成(第2.6.5 节),都是如此。为了在突然的方法调用完成时强制执行monitorentermonitorexit指令的正确配对 ,编译器生成将匹配任何异常并且其相关代码执行必要的异常处理程序(第2.10节)monitorexit 说明。

3.15。注释

class文件中注释的表示在§4.7.16 - §4.7.22中描述 。这些部分清楚地说明了如何在类,接口,字段,方法,方法参数和类型参数的声明上表示注释,以及在这些声明中使用的类型的注释。包声明的注释需要额外的规则,这里给出。

当编译器遇到必须在运行时可用的带注释的包声明时,它会发出一个class具有以下属性的文件:

该class文件代表一个接口,即, ACC_INTERFACE与ACC_ABSTRACT所述的标志ClassFile 结构被设置(§4.1)。

如果class文件版本号小于50.0,则ACC_SYNTHETIC取消设置该 标志; 如果class文件版本号是50.0或更高,则ACC_SYNTHETIC设置该标志。

该接口具有包访问权限(JLS§6.6.1)。

接口的名称是。的内部形式(§4.2.1) package-name.package-info。

该接口没有超级接口。

接口的唯一成员是Java语言规范Java SE 8 Edition(JLS§9.2)所暗示的成员。

包声明上的注释存储为 结构表中的 属性RuntimeVisibleAnnotations和RuntimeInvisibleAnnotations属性。 attributesClassFile

你可能感兴趣的:(第3章 编译Java虚拟机)