03 编译Java虚拟机

本文详细探讨了Java虚拟机(JVM)指令集的编译和执行过程,重点在于方法调用、数据类型操作、控制流程以及异常处理。通过示例代码,解释了如何将Java代码编译成JVM指令,包括如何处理局部变量、调用方法、处理数组和异常。此外,还涵盖了switch语句、finally块和同步(synchronized)块的编译方式。
摘要由CSDN通过智能技术生成

The Java Virtual Machine Specification, Java SE 8

本文介绍了用Java编程语言编写的代码编译到Java虚拟机的指令集中。

目录

一、示例格式

二、常量、局部变量和控制结构的使用

三、算术(Arithmetic)

四、访问运行时常量池(Accessing the Run-Time Constant Pool)

五、更多控制示例(More Control Examples )

六、接收参数(Receiving Arguments )

七、调用方法(Invoking Methods)

八、使用类实例(Working with Class Instances )

九、数组(Arrays)

十、编译开关(Compiling Switches )

十一、操作数在堆栈上的操作(Operations on the Operand Stack)

十二、抛出和捕获异常(Throwing and Handling Exceptions )

十三、编译 finally(Compiling finally)

十四、同步(Synchronization)

十五、注释(Annotations)


Java虚拟机被设计用来支持Java编程语言。Oracle的JDK软件包含一个编译器,从用Java编程语言编写的源代码到Java虚拟机的指令集,以及一个实现Java虚拟机本身的运行时系统。理解编译器如何利用Java虚拟机对未来的编译器编写者以及试图理解Java虚拟机本身的人都是有用的。

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

一、示例格式

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

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

<index> <opcode> [ <operand1> [ <operand2>... ]] [<comment>]

<index>是数组里指令中包含该方法的Java虚拟机代码的操作码的索引。
或者,<index>可以被认为是从方法开始的字节偏移量。
<opcode>是指令操作码的助记符;
0个或多个 <operandN>是指令的操作数。
可选的 <comment>以行末注释语法给出:

8 bipush 100 // Push int constant 100

comments中的一些材料是由javap发出的;其余的则由作者提供。
加在每个指令前面的 <index>可以用作控制传输指令的目标。
例如,一个 goto 8指令会将控制权转移到索引8处的指令上。
请注意,Java虚拟机控制传输指令的实际操作数与这些指令的操作码的地址不同;
这些操作数由 javap显示(并在本文中显示),更容易读取到它们的方法中。

我们首先介绍了一个用散列符号表示运行时常数池索引的操作数,并通过一个注释来标识所引用的运行时常数池项,如下所示:

10 ldc #1 // Push float constant 100.0

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

在本文中,我们不担心指定操作数大小等细节。

二、常量、局部变量和控制结构的使用

Java虚拟机代码展示了由Java虚拟机的设计和使用类型所施加的一组一般特征。
在第一个例子中,我们遇到许多这样的例子,我们详细地思考它们。

自旋方法只是简单地围绕一个空的循环旋转100次:

void spin() {
    int i;
    for (i = 0; i < 100; i++) {
        ; // Loop body is empty
    }
}

编译器可以编译spin编译为:

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虚拟机当前帧的操作数堆栈中获取一个或多个操作数,或者将结果推回操作数堆栈。每次调用一个方法时,都会创建一个新的帧,并使用它创建一个新的操作数堆栈和一组局部变量,供该方法使用(2.6)。因此,在计算的任何一点上,每个控制线程都可能有许多帧和等多的操作数堆栈,对应于许多嵌套的方法调用。只有当前帧中的操作数堆栈处于活动状态。

Java虚拟机的指令集通过使用不同的字节码来对其各种数据类型进行操作来区分操作数类型。
该方法的自旋只对int类型的值进行操作。在其编译代码中选择的操作类型数据(iconst_0,istore_1,iinc,iload_1,if_icmplt)的指令都是专门针对类型int的

自旋的两个常数0和100用两个不同的指令推到操作数堆栈上。
使用iconst_0指令推送0,这是iconst_<i>指令家族之一。
100使用 bipush指令进行推送,该指令获取它作为直接操作数所推送的值。

Java虚拟机经常利用某些操作数(在iconst_<i>指令中的int常数-1、0、1、2、3、4和5)的可能性,将这些操作数隐式在操作码中。因为iconst_0指令知道它将推送一个int 0,所以iconst_0不需要存储一个操作数来告诉它要推送什么值,也不需要获取或解码一个操作数。
将0的推送编译为bipush 0是正确的,但会使编译后的代码多spin旋转一个字节。
在每次循环中,简单的虚拟机还需要花费额外的时间来获取和解码显式操作数。
隐式操作数的使用使编译后的代码更加紧凑和高效。

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

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

Java虚拟机特别满足了对局部变量的某些非常频繁的操作。
iinc指令将一个本地变量的内容递增为一个1字节的有符号值。
自旋中的iinc指令将第一个局部变量(它的第一个操作数)增加到1(它的第二个操作数)。
在实现循环构造时,iinc指令非常方便。

旋转的辅助循环主要通过以下指令完成:

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之后的指令。

如果自旋示例在循环计数器中使用了int以外的数据类型,那么编译后的代码必然会更改以反映不同的数据类型。例如,如果自旋示例使用double变量来代替int,如图所示:

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指令将在本章的后面进行讨论。)

回想一下,doule占据两个局部变量,尽管它们只使用两个局部变量的较小索引访问。对于长类型的值也是如此。例如,

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虚拟机的操作码大小为1字节,因此其编译后的代码非常紧凑。
但是,1字节操作码也意味着Java虚拟机指令集必须保持较小。
作为一种妥协,Java虚拟机并不能为所有数据类型提供平等的支持:它并不是完全正交的(表2.11.1-A)。

例如,例如旋转语句中int类型值的比较可以使用一条if_icmplt指令来实现;
但是,在Java虚拟机指令集中没有一条指令对double类型的值执行有条件的分支。
因此,dspin必须使用dcmpg指令和iflt指令来实现其对doubnle类型值的比较

Java虚拟机为int类型的数据提供了最直接的支持。
这在一定程度上是为了实现Java虚拟机的操作数堆栈和局部变量数组的有效实现。
它还受到典型程序中int数据的频率的影响。其他积分类型的直接支持较少。
例如,存储、加载或添加指令中没有字节、字符或简短版本。
这是一个简短的旋转例子:

void sspin() {
    short i;
    for (i = 0; i < 100; i++) {
        ; // Loop body is empty
    }
}

它必须为Java虚拟机编译,如下所示,使用操作另一种类型的指令,很可能是int,在短值和int值之间转换,以确保对短数据的操作结果保持在适当的范围内:

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虚拟机中缺乏对字节、字符和短类型的直接支持并不是特别痛苦,因为这些类型的值在内部被提升为int(字节和短的符号扩展为int,字符为零扩展)。因此,可以在字节、字符和短数据上使用int指令进行操作。唯一的额外成本是将int操作的值截断到有效的范围。

long类型和float类型在Java虚拟机中具有中等级别的支持,只缺乏条件控制传输指令的完整补充。

三、算术(Arithmetic

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

int align2grain(int i, int grain) {
    return ((i + grain-1) & ~(grain-1));
}

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

5 iload_2 // Push grain
6 iconst_1 // Push int constant 1
7 isub // Subtract; push result
8 iconst_m1 // Push int constant -1
9 ixor // Do XOR; push result

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

整个方法的汇编代码如下:

Method 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

四、访问运行时常量池(Accessing the Run-Time Constant Pool

许多数值常量,以及对象、字段和方法,都可以通过当前类的运行时常量池进行访问。
稍后将考虑对象访问(3.8)。
int、long、float 和 double的数据类型以及对 String类实例的引用都使用 Idcldc_wldc2_w指令进行管理。

ldcldc_w指令用于访问运行时常量池中的值(包括String 类的实例)中的值。只有当存在大量的运行时常量池项,且需要一个更大的索引来访问一个项时,才会使用 ldc_w指令来代替 ldcldc2_w指令用于访问所有 double和 long类型的值;没有非宽的变体。

byte、char或 short类型的整数常数,以及小的int值,都可以使用 bipushsipushiconst_<i>指令(3.2)进行编译。某些小的浮点常量可以使用 fconst_<f>dconst_<d>指令进行编译。

在所有这些情况下,编译都很简单。例如,以下代码中的常量:

void useManyNumeric() {
 int i = 100;
 int j = 1000000;
 long l1 = 1;
 long l2 = 0xffffffff;
 double d = 2.2;
 ...do some calculations...
}

其设置方式如下:

Method void useManyNumeric()
0 bipush 100 // Push small int constant with bipush
2 istore_1
3 ldc #1 // Push large int constant (1000000) with ldc
5 istore_2
6 lconst_1 // A tiny long value uses small fast lconst_1
7 lstore_3
8 ldc2_w #6 // Push long 0xffffffff (that is, an int -1)
 // Any long constant value can be pushed with ldc2_w
11 lstore 5
13 ldc2_w #8 // Push double constant 2.200000
 // Uncommon double values are also pushed with ldc2_w
16 dstore 7
...do those calculations...

五、更多控制示例(More Control Examples

for语句的汇编显示在前面的一节(3.2)中。大多数Java编程语言的其他控件构造(if-then-else,do,while,break和continue)也以显而易见的方式编译。switch语句的编译在单独的章节(3.10)中处理,也在异常的编译(3.12)和编译的 finally子句(3.13)中处理。

作为进一步的例子,Java虚拟机以一种明显的方式编译一段 while循环,尽管Java虚拟机提供的特定控制传输指令因数据类型而不同。与往常一样,对int类型的数据有更多的支持,例如:

void whileInt() {
    int i = 0;
    while (i < 100) {
        i++;
    }
}

汇编代码为:

Method 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指令在循环的第一次迭代之前进行测试。
如果测试失败,并且从来没有输入循环体,这个额外的指令就会被浪费。
然而,虽然循环通常在预期运行其主体时使用,但通常用于多次迭代。
对于后续的迭代,将测试放在循环的底部会在每次循环中保存一条Java Virtual Machine指令:
如果测试在循环的顶部,循环体将需要一个尾部的goto指令来返回到顶部。

涉及其他数据类型的控件构造也以类似的方式编译,但必须使用这些数据类型可用的指令。
这导致代码的效率降低,因为需要更多的Java虚拟机指令,例如:

void whileDouble() {
    double i = 0.0;
    while (i < 100.1) {
        i++;
    }
}

汇编代码为:

Method 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 // Push double constant 100.1
13 dcmpg // To compare and branch we have to use...
14 iflt 5 // ...two instructions
17 return

每个浮点类型都有两个比较指令:float类型的 fcmplfcmpg,以及类型双类型的dcmpl和dcmpg。这些变异只在它们对NaN的处理上有所不同。NaN是无序的(2.3.2),因此,如果它们的任何一个操作数都是NaN,则所有浮点比较都将失败。编译器为适当的类型选择比较指令的变体,无论在非NaN值上比较失败或遇到NaN,都会产生相同的结果。例如:

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

汇编代码为:

Method int lessThan100(double)
0 dload_1
1 ldc2_w #4 // Push double constant 100.0
4 dcmpg // Push 1 if d is NaN or d > 100.0;
 // push 0 if d == 100.0
5 ifge 10 // Branch on 0 or 1
8 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) {
        return 1; 
    } else {
        return -1; 
    }
}

汇编代码为:

Method int greaterThan100(double)
0 dload_1
1 ldc2_w #4 // Push double constant 100.0
4 dcmpl // Push -1 if d is NaN or d < 100.0;
 // push 0 if d == 100.0
5 ifle 10 // Branch on 0 or -1
8 iconst_1
9 ireturn
10 iconst_m1
11 ireturn

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

六、接收参数(Receiving Arguments

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

int addTwo(int i, int j) {
    return i + j;
}

汇编代码为:

Method int addTwo(int,int)
0 iload_1 // Push value of local variable 1 (i)
1 iload_2 // Push value of local variable 2 (j)
2 iadd // Add; leave int result on operand stack
3 ireturn // Return int result

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

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

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

汇编代码为:

Method int addTwoStatic(int,int)
0 iload_0
1 iload_1
2 iadd
3 ireturn

唯一的区别是,方法参数以局部变量0开始,而不是以1开始。

七、调用方法(Invoking Methods

对实例方法的普通方法调用会对对象的运行时类型进行分派。(用C++的术语来说,它们都是虚拟的。)这样的调用是使用 invokevirtual指令实现的,该指令以运行时常量池项的索引作为其参数,给出对象的类类型的二进制名称的内部形式、要调用的方法的名称以及该方法的描述符(4.3.3)。要调用前面定义为实例方法的addTwo方法,我们可以写:

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

汇编代码为:

Method int add12and13()
0 aload_0 // Push local variable 0 (this)
1 bipush 12 // Push int constant 12
3 bipush 13 // Push int constant 13
5 invokevirtual #4 // Method Example.addtwo(II)I
8 ireturn // Return int on top of operand stack;
 // it is the int result of addTwo()

调用是通过首先将对当前实例的 reference推送到操作数堆栈来设置的。
然后推入方法调用的参数,int值12和13。
当创建了addTwo方法的框架时,传递给该方法的参数将成为新帧的局部变量的初始值。
也就是说,由调用器推送到操作数堆栈的两个参数的引用将成为调用方法的局部变量0、1和2的初始值。

最后,调用addTwo。当它返回时,它的int返回值被推到调用器的帧的操作数堆栈上,即add12and13方法。因此,返回值被立即返回到add12and13的调用器。

从添加12and13的返回由添加12和13的 ireturn指令来处理。
ireturn指令接受当前帧的操作数堆栈上的addTwo返回的int值,并将其推到调用器帧的操作数堆栈中。然后它将控制权返回给调用器,使调用器的帧处于当前帧。
Java虚拟机为其许多数字和 reference数据类型提供了不同的返回指令,并为没有返回值的方法提供了返回指令。相同的返回指令集被用于所有种类的方法调用。

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

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

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

虽然使用了一个不同的Java虚拟机方法调用指令:

Method int add12and13()
0 bipush 12
2 bipush 13
4 invokestatic #3 // Method Example.addTwoStatic(II)I
7 ireturn

编译对类(static)方法的调用非常像编译对实例方法的调用,只是这不是由调用者传递的。
因此,方法参数将以局部变量0(3.6)开头进行接收。invokestatic指令总是用于调用类方法。

必须使用调用特殊指令来调用实例初始化方法(3.8)。在调用超类(super)中的方法和调用private方法时,也会使用它。例如,给定的类Near和Far声明为:

class Near {
    int it;
    public int getItNear() {
        return getIt();
    }
    private int getIt() {
        return it;
    }
}
class Far extends Near {
    int getItFar() {
        return super.getItNear();
    }
}

方法Near.getItNear(它调用私有方法)变成:

Method int getItNear()
0 aload_0
1 invokespecial #5 // Method Near.getIt()I
4 ireturn

方法Far.getItFar(它调用超类方法)变成:

Method int getItFar()
0 aload_0
1 invokespecial #4 // Method Near.getItNear()I
4 ireturn

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

要调用方法句柄的目标,编译器必须形成一个方法描述符来记录实际的参数和返回类型。
编译器可能不会对参数执行方法调用转换;
相反,它必须根据它们自己的未转换类型将它们推到堆栈上。
像往常一样,编译器将对方法句柄对象的引用推送到堆栈上。
编译器发出调用指令,引用描述参数和返回类型的描述符。
通过与方法解析(5.4.3.3)的特殊安排,一种调用调用精确或调用java.lang.invoke方法的调用虚拟指令。
如果方法描述符语法格式良好,并且描述符中命名的类型可以被解析,那么方法句柄将始终链接。

八、使用类实例(Working with Class Instances

Java虚拟机类实例是使用Java虚拟机的新指令创建的。回想一下,在Java虚拟机级别,构造函数以编译器提供的名称<init>的方法出现。这个特殊命名的方法被称为实例初始化方法(2.9)。对于一个给定的类,可能存在对应于多个构造函数的多个实例初始化方法。一旦创建了类实例,其实例变量,包括类及其所有超类的变量都被初始化为默认值,就会调用新类实例的实例初始化方法。例如:

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

汇编代码为:

Method java.lang.Object create()
0 new #1 // Class java.lang.Object
3 dup
4 invokespecial #4 // Method java.lang.Object.<init>()V
7 areturn

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

int i; // An instance variable
MyObj example() {
    MyObj o = new MyObj();
    return silly(o);
}
MyObj silly(MyObj o) {
    if (o != null) {
        return o;
    } else {
        return o;
    }
}

汇编代码为:

Method MyObj example()
0 new #2 // Class MyObj
3 dup
4 invokespecial #5 // Method MyObj.<init>()V
7 astore_1
8 aload_0
9 aload_1
10 invokevirtual #4 // Method Example.silly(LMyObj;)LMyObj;
13 areturn
Method MyObj silly(MyObj)
0 aload_1
1 ifnull 6
4 aload_1
5 areturn
6 aload_1
7 areturn

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

void setIt(int value) {
   i = value;
}
int getIt() {
   return i;
}

汇编代码为:

Method void setIt(int)
0 aload_0
1 iload_1
2 putfield #4 // Field Example.i I
5 return
Method int getIt()
0 aload_0
1 getfield #4 // Field Example.i I
4 ireturn

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

九、数组(Arrays)

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

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

可编译为:

Method void createBuffer()
0 bipush 100 // Push int constant 100 (bufsz)
2 istore_2 // Store bufsz in local variable 2
3 bipush 12 // Push int constant 12 (value)
5 istore_3 // Store value in local variable 3
6 iload_2 // Push bufsz...
7 newarray int // ...and create new int array of that length
9 astore_1 // Store new array in buffer
10 aload_1 // Push buffer
11 bipush 10 // Push int constant 10
13 iload_3 // Push value
14 iastore // Store value at buffer[10]
15 aload_1 // Push buffer
16 bipush 11 // Push int constant 11
18 iaload // Push value at buffer[11]...
19 istore_3 // ...and store it in value
20 return

anewarray指令用于创建一维对象引用数组,例如:

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

汇编代码为:

Method void createThreadArray()
0 bipush 10 // Push int constant 10
2 istore_2 // Initialize count to that
3 iload_2 // Push count, used by anewarray
4 anewarray class #1 // Create new array of class Thread
7 astore_1 // Store new array in threads
8 aload_1 // Push value of threads
9 iconst_0 // Push int constant 0
10 new #1 // Create instance of class Thread
13 dup // Make duplicate reference...
14 invokespecial #5 // ...for Thread's constructor
 // Method java.lang.Thread.<init>()V
17 aastore // Store new Thread in array at 0
18 return

anewarray指令也可以用于创建多维数组的第一个维度。
或者,可以使用多核序列指令一次创建多个维度。例如,三维数组:

多重序列阵列指令的第一个操作数是对要创建的数组类类型的运行时常量池索引。
第二个维度是要实际创建的该数组类型的维数。多重小序列指令可用于创建该类型的所有维度,如create3DArray的代码所示。
请注意,多维数组只是一个对象,因此分别由aload_1和区域指令加载和返回。
有关数组类名的信息,请参见4.4.1。

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

十、编译开关(Compiling Switches

开关语句的编译使用表开关和查找开关指令。当开关的情况可以有效地表示为目标偏移量表中的索引时,将使用表开关指令。如果开关的表达式的值超出了有效索引的范围,则使用开关的默认目标。例如:

int chooseNear(int i) {
    switch (i) {
        case 0: return 0;
        case 1: return 1;
        case 2: return 2;
        default: return -1;
    }
}

汇编代码为:

Method int chooseNear(int)
0 iload_1 // Push local variable 1 (argument i)
1 tableswitch 0 to 2: // Valid indices are 0 through 2
 0: 28 // If i is 0, continue at 28
 1: 30 // If i is 1, continue at 30
 2: 32 // If i is 2, continue at 32
 default:34 // Otherwise, continue at 34
28 iconst_0 // i was 0; push int constant 0...
29 ireturn // ...and return it
30 iconst_1 // i was 1; push int constant 1...
31 ireturn // ...and return it
32 iconst_2 // i was 2; push int constant 2...
33 ireturn // ...and return it
34 iconst_m1 // otherwise push int constant -1...
35 ireturn // ...and return it

Java虚拟机的表开关和查找开关指令只对int数据进行操作。
因为对字节、字符或短值的操作在内部提升为int,所以汇编表达式计算为这些类型之一的开关,就像计算为输入int一样。
如果选择的near方法是使用类型短写的,那么将生成与使用类型int时相同的Java虚拟机指令。其他数字类型必须缩小为输入int,以便在开关中使用。

当开关的情况是稀疏的时,表开关指令的表表示在空间上变得低效。
可以使用查找开关指令来代替。查找开关指令将int键(案例标签的值)与表中的目标偏移量对。
当执行查找开关指令时,将该开关的表达式的值与表中的键进行比较。
如果其中一个键与表达式的值匹配,则在关联的目标偏移量处继续执行。
如果没有键匹配,则在默认目标上继续执行。例如,编译的代码:

int chooseFar(int i) {
    switch (i) {
        case -100: return -1;
        case 0: return 0;
        case 100: return 1;
        default: return -1;
    }
}

看起来就像chooseNear的代码,除了lookupswitch指令:

Method int chooseFar(int)
0 iload_1
1 lookupswitch 3:
 -100: 36
 0: 38
 100: 40
 default: 42
36 iconst_m1
37 ireturn
38 iconst_0
39 ireturn
40 iconst_1
41 ireturn
42 iconst_m1
43 ireturn

Java虚拟机指定查找开关指令的表必须按键进行排序,以便实现可以使用比线性扫描更有效的搜索。即便如此,查找开关指令也必须搜索其键以寻找匹配项,而不是简单地像表开关那样在表中执行边界检查和索引。因此,表开关指令可能比考虑空间因素允许选择的查找开关更有效。

十一、操作数在堆栈上的操作(Operations on the Operand Stack

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

public long nextIndex() { 
    return index++;
}
private long index = 0;

汇编代码为:

Method long nextIndex()
0 aload_0 // Push this
1 dup // Make a copy of it
2 getfield #4 // One of the copies of this is consumed
 // pushing long field index,
 // above the original this
5 dup2_x1 // The long on top of the operand stack is 
 // inserted into the operand stack below the 
 // original this
6 lconst_1 // Push long constant 1 
7 ladd // The index value is incremented...
8 putfield #4 // ...and the result stored in the field
11 lreturn // The original value of index is on top of
 // the operand stack, ready to be returned

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

十二、抛出和捕获异常(Throwing and Handling Exceptions

使用抛出关键字从程序中抛出异常。它的代码很简单:

void cantBeZero(int i) throws TestExc {
    if (i == 0) {
        throw new TestExc();
    }
}

汇编代码为:

Method void cantBeZero(int)
0 iload_1 // Push argument 1 (i)
1 ifne 12 // If i==0, allocate instance and throw
4 new #1 // Create instance of TestExc
7 dup // One reference goes to its constructor
8 invokespecial #7 // Method TestExc.<init>()V
11 athrow // Second reference is thrown
12 return // Never get here if we threw TestExc

尝试捕获构造的编译很简单。例如:

void catchOne() {
    try {
        tryItOut();
    } catch (TestExc e) {
        handleExc(e);
    }
}

汇编代码为:

Method void catchOne()
0 aload_0 // Beginning of try block
1 invokevirtual #6 // Method Example.tryItOut()V
4 return // End of try block; normal return
5 astore_1 // Store thrown value in local var 1
6 aload_0 // Push this
7 aload_1 // Push thrown value
8 invokevirtual #5 // Invoke handler method: 
 // Example.handleExc(LTestExc;)V
11 return // Return after handling TestExc
Exception table:
From To Target Type
0 4 5 Class TestExc

更仔细地看,尝试块是编译的,就像如果尝试不存在一样:

Method void catchOne()
0 aload_0 // Beginning of try block
1 invokevirtual #6 // Method Example.tryItOut()V
4 return // End of try block; normal return

如果在执行尝试块期间没有抛出异常,它的行为就像尝试不在一样:调用 trytotOut和 catchOne返回。

在try块之后是实现单个catch子句的Java虚拟机代码:

5 astore_1 // Store thrown value in local var 1
6 aload_0 // Push this
7 aload_1 // Push thrown value
8 invokevirtual #5 // Invoke handler method: 
 // Example.handleExc(LTestExc;)V
11 return // Return after handling TestExc
Exception table:
From To Target Type
0 4 5 Class TestExc

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

一个尝试可能会有多个catch子句:

void catchTwo() {
    try {
        tryItOut();
    } catch (TestExc1 e) {
        handleExc(e);
    } catch (TestExc2 e) {
        handleExc(e);
    }
}

一个给定的try语句的多个catth子句是通过简单地为每个捕获器逐个附加Java虚拟机代码子句并添加到异常表来编译的,如下所示:

Method void catchTwo()
0 aload_0 // Begin try block
1 invokevirtual #5 // Method Example.tryItOut()V
4 return // End of try block; normal return
5 astore_1 // Beginning of handler for TestExc1;
 // Store thrown value in local var 1
6 aload_0 // Push this
7 aload_1 // Push thrown value
8 invokevirtual #7 // Invoke handler method:
 // Example.handleExc(LTestExc1;)V
11 return // Return after handling TestExc1
12 astore_1 // Beginning of handler for TestExc2;
 // Store thrown value in local var 1
13 aload_0 // Push this
14 aload_1 // Push thrown value
15 invokevirtual #7 // Invoke handler method:
 // Example.handleExc(LTestExc2;)V
18 return // Return after handling TestExc2
Exception table:
From To Target Type
0 4 5 Class TestExc1
0 4 12 Class TestExc2

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

嵌套的try-catch语句的编译方式非常像一个带有多个catch子句的try语句:

void nestedCatch() {
    try {
        try {
            tryItOut();
        } catch (TestExc1 e) {
            handleExc1(e);
        }
    } catch (TestExc2 e) {
        handleExc2(e);
    }
}

汇编代码为:

Method void nestedCatch()
0 aload_0 // Begin try block
1 invokevirtual #8 // Method Example.tryItOut()V
4 return // End of try block; normal return
5 astore_1 // Beginning of handler for TestExc1;
 // Store thrown value in local var 1
6 aload_0 // Push this
7 aload_1 // Push thrown value
8 invokevirtual #7 // Invoke handler method: 
 // Example.handleExc1(LTestExc1;)V
11 return // Return after handling TestExc1
12 astore_1 // Beginning of handler for TestExc2;
 // Store thrown value in local var 1
13 aload_0 // Push this
14 aload_1 // Push thrown value
15 invokevirtual #6 // Invoke handler method:
 // Example.handleExc2(LTestExc2;)V
18 return // Return after handling TestExc2
Exception table:
From To Target Type
0 4 5 Class TestExc1
0 12 12 Class TestExc2

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

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

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

十三、编译 finally(Compiling finally)

(本节假定编译器生成版本号为50.0或以下的类文件,以便可以使用 jsr指令。另见Subab 4.10.2.5.)

最后try-finally语句的编译类似于尝试捕获的编译。在在try语句之外传输控制之前,无论该传输是正常的还是突然的,因为已经抛出了异常,所以必须首先执行最终子句。对于这个简单的示例:

void tryFinally() {
    try {
        tryItOut();
    } finally {
        wrapItUp();
    }
}

汇编代码为:

Method void tryFinally()
0 aload_0 // Beginning of try block
1 invokevirtual #6 // Method Example.tryItOut()V
4 jsr 14 // Call finally block
7 return // End of try block
8 astore_1 // Beginning of handler for any throw
9 jsr 14 // Call finally block
12 aload_1 // Push thrown value
13 athrow // ...and rethrow value to the invoker
14 astore_2 // Beginning of finally block
15 aload_0 // Push this
16 invokevirtual #5 // Method Example.wrapItUp()V
19 ret 2 // Return from finally block
Exception table:
From To Target Type
0 4 8 any

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

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

带有filnally子句的try语句被编译为有一个特殊的异常处理程序,它可以处理try语句中抛出的任何异常。如果tryItOut抛出异常,则在tryFinally的异常表中搜索适当的异常处理程序。找到了特殊的处理程序,这导致在索引8处继续执行。索引8处的astore_1指令将抛出的值存储到局部变量1中。下面的jsr指令对最后一个块的代码进行子例程调用。假设代码正常返回,索引12处的aload_1指令将抛出的值推回操作数堆栈,下面的路径指令重新抛出该值。

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

void tryCatchFinally() {
    try {
        tryItOut();
    } catch (TestExc e) {
        handleExc(e);
    } finally {
        wrapItUp();
    }
}

成为:

Method void tryCatchFinally()
0 aload_0 // Beginning of try block
1 invokevirtual #4 // Method Example.tryItOut()V
4 goto 16 // Jump to finally block
7 astore_3 // Beginning of handler for TestExc;
 // Store thrown value in local var 3
8 aload_0 // Push this
9 aload_3 // Push thrown value
10 invokevirtual #6 // Invoke handler method:
 // Example.handleExc(LTestExc;)V
13 goto 16 // This goto is unnecessary, but was
 // generated by javac in JDK 1.0.2
16 jsr 26 // Call finally block
19 return // Return after handling TestExc
20 astore_1 // Beginning of handler for exceptions
 // other than TestExc, or exceptions
 // thrown while handling TestExc
21 jsr 26 // Call finally block
24 aload_1 // Push thrown value...
25 athrow // ...and rethrow value to the invoker
26 astore_2 // Beginning of finally block
27 aload_0 // Push this
28 invokevirtual #5 // Method Example.wrapItUp()V
31 ret 2 // Return from finally block
Exception table:
From To Target Type
0 4 7 Class TestExc
0 16 20 any

如果try语句正常完成,则索引4处的goto指令将跳转到索引16处的finally块的子例程调用中。
最后执行索引26处的块,控制返回到索引19的返回指令,并尝试捕获最后正常返回。

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

如果tryItOut抛出的一个值不是TestExc的实例,或者把handleExc本身抛出一个异常,该条件由异常表中的第二个条目处理,该项处理索引0和16之间抛出的任何值。该异常处理程序将控制转移到索引20,其中抛出的值首先存储在本地变量1中。在索引26处的finally块的代码被称为一个子例程。如果返回,则从局部变量1检索抛出的值,并使用路径指令重新抛出。如果在执行finally子句期间抛出新值,finally子句将终止,然后tryCatchFinally突然返回,将新值抛出给其调用器。

十四、同步(Synchronization

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

对于用Java编程语言编写的代码,也许最常见的同步形式是synchronized方法。
synchronized的方法通常不使用监控器和监控器退出来实现。
相反,它只是在运行时常量池中通过ACC_SYNCHRONIZED标志来区分,该标志由方法调用指令(2.11.10)进行检查。

monitorentermonitorexit指令,可开启synchronized语句的编译。例如:

void onlyMe(Foo f) {
    synchronized(f) {
        doSomething();
    }
}

编译为:

Method void onlyMe(Foo)
0 aload_1 // Push f
1 dup // Duplicate it on the stack
2 astore_2 // Store duplicate in local variable 2
3 monitorenter // Enter the monitor associated with f
4 aload_0 // Holding the monitor, pass this and...
5 invokevirtual #5 // ...call Example.doSomething()V
8 aload_2 // Push local variable 2 (f)
9 monitorexit // Exit the monitor associated with f
10 goto 18 // Complete the method normally
13 astore_3 // In case of any throw, end up here
14 aload_2 // Push local variable 2 (f)
15 monitorexit // Be sure to exit the monitor!
16 aload_3 // Push thrown value...
17 athrow // ...and rethrow value to the invoker
18 return // Return in the normal case
Exception table:
From To Target Type
4 10 13 any
13 16 13 any

编译器确保在任何方法调用完成时,对自方法调用以来执行的每个监视器指令都将执行一个monitorenter退出指令。此时,方法调用是正常完成(2.6.4)还是突然完成(2.6.5)。为了在突然的方法调用完成时强制 monitorenter和 monitorexit指令的正确配对,编译器生成异常处理程序(2.10),其相关的代码执行必要的 monitorexit指令。

十五、注释(Annotations

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

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

  • class 文件表示一个接口,即设置了 ClassFile 结构的 ACC_INTERFACE 和 ACC_ABSTRACT标志(4.1)。
  • 如果 ClassFile 版本号小于50.0,则会不设置ACC_SYNTHETIC标志;
    如果 ClassFile 版本号为50.0或以上,则将会设置ACC_SYNTHETIC标志
  • 该接口具有软件包访问权限(JLS 6.6.1)
  • 该接口的名称是package-name.packageinfo的内部形式(4.2.1)。
  • 该接口没有超接口。
  • 该接口的唯一成员是那些由Java语言规范,Java SE 8版(JLS 9.2)所隐含的成员。
  • 软件包声明上的注释作为运行时可见注释和RuntimeInvisibleAnnotations属性存储在 ClassFile结构的属性表中。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值