Appearance
第3章 为Java虚拟机编译
提示
来自deepseek解释
原文链接:https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-3.html
Java虚拟机被设计为支持Java编程语言。Oracle的JDK软件包含一个编译器,用于将Java编程语言编写的源代码编译为Java虚拟机的指令集,同时还包含一个实现Java虚拟机本身的运行时系统。理解编译器如何利用Java虚拟机,对于未来的编译器编写者以及试图理解Java虚拟机本身的人来说都是很有帮助的。本章中带编号的章节不是规范性的。
注意,“编译器”一词有时也用来指将Java虚拟机指令集翻译为特定CPU指令集的转换器。这种转换器的一个例子是即时(JIT)代码生成器,它只在Java虚拟机代码被加载后才生成特定于平台的指令。本章不讨论与代码生成相关的问题,只讨论将Java编程语言编写的源代码编译为Java虚拟机指令的相关问题。
3.1 示例格式
本章主要由源代码示例以及Oracle JDK 1.0.2版本中javac编译器为这些示例生成的Java虚拟机代码的带注释列表组成。Java虚拟机代码以Oracle的javap工具输出的非正式的“虚拟机汇编语言”编写,该工具随JDK发布。你可以使用javap生成更多已编译方法的示例。
示例的格式对于任何读过汇编代码的人来说应该都很熟悉。每条指令采用以下形式:
<索引> <操作码> [ <操作数1> [ <操作数2>... ]] [ <注释> ]<索引>是该指令的操作码在包含此方法Java虚拟机代码字节数组中的索引。或者,<索引>也可以看作是从方法开头开始的字节偏移量。<操作码>是指令操作码的助记符,零个或多个<操作数N>是指令的操作数。可选的<注释>以行尾注释语法给出:
8 bipush 100 // Push int constant 100注释中的部分内容由javap输出;其余部分由作者提供。每条指令前面的<索引>可以用作控制转移指令的目标。例如,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方法简单地在一个空的for循环中自旋100次:
java
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 doneJava虚拟机是基于栈的,大多数操作从Java虚拟机当前帧的操作数栈中取出一个或多个操作数,或将结果压回操作数栈。每次调用方法时都会创建一个新的帧,并随之创建一个新的操作数栈和一组供该方法使用的局部变量(§2.6)。在计算的任何时刻,每个控制线程中很可能有许多帧和同样多的操作数栈,对应于许多嵌套的方法调用。只有当前帧中的操作数栈是活动的。
Java虚拟机的指令集通过对其各种数据类型使用不同的字节码来区分操作数类型。spin方法只操作int类型的值。其编译代码中选择的操作类型化数据的指令(iconst_0、istore_1、iinc、iload_1、if_icmplt)都是专门针对int类型的。
spin中的两个常量0和100使用两条不同的指令压入操作数栈。0使用iconst_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的编译代码长一个字节。一个简单的虚拟机在每次循环时还需要花费额外的时间来获取和解码显式操作数。使用隐含操作数使编译代码更紧凑、更高效。
spin中的int变量i存储为Java虚拟机局部变量1。因为大多数Java虚拟机指令操作的是从操作数栈弹出的值,而不是直接操作局部变量,所以在为Java虚拟机编译的代码中,在局部变量和操作数栈之间传输值的指令很常见。这些操作在指令集中也有特殊的支持。在spin中,使用istore_1和iload_1指令在局部变量之间传输值,每条指令都隐式地操作局部变量1。istore_1指令从操作数栈弹出一个int并将其存储到局部变量1中。iload_1指令将局部变量1中的值压入操作数栈。
局部变量的使用(和重用)是编译器编写者的责任。专门的加载和存储指令应鼓励编译器编写者尽可能多地重用局部变量。生成的代码更快、更紧凑,并且在帧中占用更少的空间。
Java虚拟机特别为局部变量上的某些非常频繁的操作提供了支持。iinc指令将局部变量的内容增加一个一字节的有符号值。spin中的iinc指令将第一个局部变量(其第一个操作数)增加1(其第二个操作数)。iinc指令在实现循环结构时非常方便。
spin的for循环主要由以下指令完成:
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示例使用double而不是int,如下所示:
java
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值占用两个局部变量,尽管它们只使用两个局部变量中较小的索引来访问。long类型的值也是如此。再例如:
java
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)。
例如,示例spin中for语句中对int类型值的比较可以使用单个if_icmplt指令来实现;但是,Java虚拟机指令集中没有单个指令可以对double类型的值执行条件分支。因此,dspin必须使用dcmpg指令后跟iflt指令来实现对double类型值的比较。
Java虚拟机对int类型的数据提供了最直接的支持。这部分是预期到Java虚拟机操作数栈和局部变量数组的高效实现。这也是由典型程序中int数据的频繁使用所驱动的。其他整数类型的支持不那么直接。例如,没有byte、char或short版本的存储、加载或加法指令。下面是使用short编写的spin示例:
java
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 returnJava虚拟机缺乏对byte、char和short类型的直接支持,但这并不是特别麻烦,因为这些类型的值在内部被提升为int(byte和short被符号扩展为int,char被零扩展为int)。因此,对byte、char和short数据的操作可以使用int指令来完成。唯一额外的代价是将int操作的结果截断到有效范围。
long和浮点类型在Java虚拟机中具有中等程度的支持,只缺少完整的条件控制转移指令集。
3.3 算术运算
Java虚拟机通常在其操作数栈上执行算术运算。(例外是iinc指令,它直接递增局部变量的值。)例如,align2grain方法将一个int值对齐到给定的2的幂:
java
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 ireturn3.4 访问运行时常量池
许多数值常量以及对象、字段和方法都通过当前类的运行时常量池来访问。对象访问将在§3.8中讨论。int、long、float和double类型的数据,以及对String类实例的引用,都使用ldc、ldc_w和ldc2_w指令来管理。
ldc和ldc_w指令用于访问运行时常量池中double和long以外类型的值(包括String类的实例)。只有当运行时常量池项数量很大且需要更大的索引来访问某项时,才使用ldc_w指令代替ldc。ldc2_w指令用于访问所有double和long类型的值;没有非宽体变种。
byte、char或short类型的整数常量,以及小的int值,可以使用bipush、sipush或iconst_<i>指令编译(§3.2)。某些小的浮点常量可以使用fconst_<f>和dconst_<d>指令编译。
在所有这些情况下,编译都是直接的。例如,以下常量:
java
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...3.5 更多控制示例
for语句的编译已在前面章节(§3.2)中展示。Java编程语言的大多数其他控制结构(if-then-else、do、while、break和continue)也以显而易见的方式编译。switch语句的编译在单独章节(§3.10)中处理,异常(§3.12)和finally子句(§3.13)的编译也是如此。
作为进一步的示例,while循环以显而易见的方式编译,尽管Java虚拟机提供的特定控制转移指令因数据类型而异。一如既往,对int类型的数据有更多支持,例如:
java
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指令到达测试。如果测试失败,并且循环体从未进入,则这条额外指令就浪费了。然而,while循环通常在其体预期运行时使用,通常用于许多次迭代。对于后续迭代,将测试放在循环底部每次循环节省了一条Java虚拟机指令:如果测试在循环顶部,循环体将需要一个尾随的goto指令才能回到顶部。
涉及其他数据类型的控制结构以类似的方式编译,但必须使用这些数据类型可用的指令。这导致代码效率稍低,因为需要更多的Java虚拟机指令,例如:
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类型有fcmpl和fcmpg,对于double类型有dcmpl和dcmpg。变体的区别仅在于它们对NaN的处理。NaN是无序的(§2.3.2),因此如果任一操作数为NaN,所有浮点比较都会失败。编译器为适当的类型选择比较指令的变体,使得无论比较是在非NaN值上失败还是遇到NaN,都能产生相同的结果。例如:
java
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指令达到相同的效果:
java
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。
3.6 接收参数
如果有n个参数传递给一个实例方法,按照约定,它们在被调用方法的帧的局部变量编号1到n中接收。参数按照传递的顺序接收。例如:
java
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中传递一个对其实例的引用。在Java编程语言中,该实例可通过this关键字访问。
类(静态)方法没有实例,因此对它们来说,局部变量0的这种使用是不必要的。类方法从索引0开始使用局部变量。如果addTwo方法是类方法,其参数的传递方式与第一个版本类似:
java
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开始出现。
3.7 调用方法
实例方法的正常方法调用根据对象的运行时类型进行分派(用C++术语来说,它们是虚的)。这种调用使用invokevirtual指令实现,该指令以运行时常量池索引作为其参数,该索引给出对象的类类型的二进制名称的内部形式、要调用的方法名称以及该方法的描述符(§4.3.3)。要调用前面定义为实例方法的addTwo方法,我们可以这样写:
java
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()调用的设置是首先将对当前实例this的引用压入操作数栈。然后压入方法调用的参数,即int值12和13。当为addTwo方法创建帧时,传递给该方法的参数将成为新帧局部变量的初始值。也就是说,调用者压入操作数栈的this引用和两个参数将成为被调用方法局部变量0、1和2的初始值。
最后,调用addTwo。当它返回时,其int返回值被压入调用者add12and13方法的帧的操作数栈。因此,返回值被放置在适当位置,以便立即返回给add12and13的调用者。
从add12and13返回由add12and13的ireturn指令处理。ireturn指令获取addTwo返回的int值(位于当前帧的操作数栈上),并将其压入调用者帧的操作数栈。然后它将控制返回给调用者,使调用者的帧成为当前帧。Java虚拟机为其许多数值和引用数据类型提供了不同的返回指令,并为无返回值的方法提供了return指令。相同的返回指令集用于所有类型的方法调用。
invokevirtual指令的操作数(在示例中是运行时常量池索引#4)不是类实例中方法的偏移量。编译器不知道类实例的内部布局。相反,它生成对实例方法的符号引用,这些引用存储在运行时常量池中。这些运行时常量池项在运行时被解析以确定实际的方法位置。对于访问类实例的所有其他Java虚拟机指令也是如此。
调用addTwoStatic(addTwo的类(静态)变体)类似,如下所示:
java
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编译对类(静态)方法的调用与编译对实例方法的调用非常相似,只是调用者不传递this。因此,方法参数将从局部变量0开始接收(§3.6)。invokestatic指令始终用于调用类方法。
invokespecial指令必须用于调用实例初始化方法(§3.8)。它也用于调用超类中的方法(super)。例如,给定如下声明的类Near和Far:
java
class Near {
int it;
int getItNear() {
return it;
}
}
class Far extends Near {
int getItFar() {
return super.getItNear();
}
}方法Far.getItFar(它调用一个超类方法)变为:
Method int getItFar()
0 aload_0
1 invokespecial #4 // Method Near.getItNear()I
4 ireturn注意,使用invokespecial指令调用的方法总是将this作为其第一个参数传递给被调用方法。按照惯例,它在局部变量0中接收。
要调用方法句柄的目标,编译器必须形成一个记录实际参数和返回类型的方法描述符。编译器不得对参数执行方法调用转换;相反,必须根据它们自己的未转换类型将它们压入栈。编译器照常安排在参数之前将对方法句柄对象的引用压入栈。编译器发出一个invokevirtual指令,引用一个描述参数和返回类型的描述符。通过与方法解析的特殊安排(§5.4.3.3),调用java.lang.invoke.MethodHandle的invokeExact或invoke方法的invokevirtual指令将始终链接,前提是方法描述符在语法上是良构的,并且描述符中命名的类型可以被解析。
3.8 使用类实例
Java虚拟机类实例使用Java虚拟机的new指令创建。回想一下,在Java虚拟机层面,构造函数表现为一个具有编译器提供的名称<init>的方法。这个特殊命名的方法被称为实例初始化方法(§2.9)。对于一个给定的类,可能存在多个实例初始化方法,对应于多个构造函数。一旦类实例被创建并且其实例变量(包括该类及其所有超类的实例变量)被初始化为其默认值,就会调用新类实例的实例初始化方法。例如:
java
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类实例作为引用类型传递和返回,与数值非常相似,尽管引用类型有自己的指令集,例如:
java
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类实例的字段(实例变量)使用getfield和putfield指令访问。如果i是一个int类型的实例变量,则定义的方法setIt和getIt:
java
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与方法调用指令的操作数一样,putfield和getfield指令的操作数(运行时常量池索引#4)不是类实例中字段的偏移量。编译器生成对实例字段的符号引用,这些引用存储在运行时常量池中。这些运行时常量池项在运行时被解析以确定所引用对象中字段的位置。
3.9 数组
Java虚拟机数组也是对象。数组使用一组不同的指令创建和操作。newarray指令用于创建数值类型的数组。代码:
java
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 returnanewarray指令用于创建对象引用的一维数组,例如:
java
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 returnanewarray指令也可用于创建多维数组的第一维。或者,multianewarray指令可用于一次创建多个维度。例如,三维数组:
java
int[][][] create3DArray() {
int grid[][][];
grid = new int[10][5][];
return grid;
}由以下方式创建:
Method int create3DArray()[][][]
0 bipush 10 // Push int 10 (dimension one)
2 iconst_5 // Push int 5 (dimension two)
3 multianewarray #1 dim #2 // Class [[[I, a three-dimensional
// int array; only create the
// first two dimensions
7 astore_1 // Store new array...
8 aload_1 // ...then prepare to return it
9 areturnmultianewarray指令的第一个操作数是运行时常量池中要创建的数组类类型的索引。第二个是要实际创建的该数组类型的维度数。multianewarray指令可用于创建该类型的所有维度,如create3DArray的代码所示。注意,多维数组只是一个对象,因此分别由aload_1和areturn指令加载和返回。关于数组类名称的信息,请参见§4.4.1。
所有数组都有相关的长度,通过arraylength指令访问。
3.10 编译Switch语句
switch语句的编译使用tableswitch和lookupswitch指令。当switch的case可以有效地表示为目标偏移量表索引时,使用tableswitch指令。如果switch表达式的值落在有效索引范围之外,则使用switch的默认目标。例如:
java
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 itJava虚拟机的tableswitch和lookupswitch指令仅操作int数据。因为对byte、char或short值的操作在内部被提升为int,所以表达式求值为这些类型之一的switch被编译为好像它求值为int类型。如果chooseNear方法是用short类型编写的,将生成与使用int类型时相同的Java虚拟机指令。其他数值类型必须缩窄为int类型才能在switch中使用。
当switch的case稀疏时,tableswitch指令的表表示在空间方面变得低效。此时可以使用lookupswitch指令。lookupswitch指令在一个表中将int键(case标签的值)与目标偏移量配对。当执行lookupswitch指令时,将switch表达式的值与表中的键进行比较。如果其中一个键与表达式的值匹配,则继续执行关联的目标偏移量。如果没有键匹配,则继续执行默认目标。例如,以下代码的编译代码:
java
int chooseFar(int i) {
switch (i) {
case -100: return -1;
case 0: return 0;
case 100: return 1;
default: return -1;
}
}除了lookupswitch指令外,看起来与chooseNear的代码相同:
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 ireturnJava虚拟机规定lookupswitch指令的表必须按键排序,以便实现可以使用比线性扫描更高效的搜索。即便如此,lookupswitch指令必须在其键中搜索匹配项,而不是像tableswitch那样简单地执行边界检查并索引到表中。因此,在空间考虑允许选择的情况下,tableswitch指令可能比lookupswitch更高效。
3.11 操作数栈上的操作
Java虚拟机有大量的指令,将操作数栈的内容作为无类型值进行操作。这些指令很有用,因为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虚拟机绝不允许其操作数栈操作指令修改或拆分操作数栈上的单个值。
3.12 抛出和处理异常
异常使用throw关键字从程序中抛出。其编译很简单:
java
void cantBeZero(int i) throws TestExc {
if (i == 0) {
throw new TestExc();
}
}变为:
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 TestExctry-catch结构的编译很简单。例如:
java
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更仔细地看,try块的编译方式与没有try时完全相同:
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如果在try块执行期间没有抛出异常,它的行为就像没有try一样:调用tryItOut,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 TestExchandleExc的调用,即catch子句的内容,也像普通方法调用一样编译。然而,catch子句的存在导致编译器生成一个异常表项(§2.10,§4.7.3)。catchOne方法的异常表有一个对应于catchOne的catch子句可以处理的一个参数(TestExc类的实例)的条目。如果在catchOne中索引0到4之间的指令执行期间抛出了某个是TestExc实例的值,控制转移到索引5处的Java虚拟机代码,该代码实现了catch子句的块。如果抛出的值不是TestExc的实例,catchOne的catch子句无法处理它。相反,该值被重新抛出给catchOne的调用者。
一个try可以有多个catch子句:
java
void catchTwo() {
try {
tryItOut();
} catch (TestExc1 e) {
handleExc(e);
} catch (TestExc2 e) {
handleExc(e);
}
}给定try语句的多个catch子句通过简单地将每个catch子句的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的任何catch子句的参数都不匹配,Java虚拟机重新抛出该值,而不调用catchTwo的任何catch子句中的代码。
嵌套的try-catch语句的编译与具有多个catch子句的try语句非常相似:
java
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 TestExc2catch子句的嵌套仅体现在异常表中。Java虚拟机不强制异常表项的嵌套或任何排序(§2.10)。然而,由于try-catch构造是结构化的,编译器总是可以对异常处理表的条目进行排序,使得对于任何抛出的异常和该方法中的任何程序计数器值,与抛出异常匹配的第一个异常处理器对应于最内层的匹配catch子句。
例如,如果tryItOut的调用(在索引1处)抛出了一个TestExc1的实例,它将由调用handleExc1的catch子句处理。即使异常发生在外层catch子句(捕获TestExc2)的范围内,并且即使该外层catch子句本来能够处理抛出的值,也是如此。
作为一个细微的点,注意catch子句的范围在“from”端是包含的,在“to”端是排除的(§4.7.3)。因此,捕获TestExc1的catch子句的异常表项不覆盖偏移量4处的return指令。然而,捕获TestExc2的catch子句的异常表项覆盖偏移量11处的return指令。嵌套catch子句内的return指令包含在嵌套catch子句覆盖的指令范围内。
3.13 编译finally子句
(本节假设编译器生成版本号为50.0或以下的类文件,以便可以使用jsr指令。另见§4.10.2.5。)
try-finally语句的编译与try-catch类似。在控制转移出try语句之前,无论该转移是正常的还是突然的(因为抛出了异常),都必须首先执行finally子句。对于这个简单的例子:
java
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语句:通过从该块的底部流出、通过返回、通过执行break或continue语句,或者通过引发异常。如果tryItOut返回而没有引发异常,则使用jsr指令将控制转移到finally块。索引4处的jsr 14指令对索引14处的finally块代码进行“子程序调用”(finally块被编译为嵌入的子程序)。当finally块完成时,ret 2指令将控制返回到索引4处jsr指令后面的指令。
更详细地说,子程序调用的工作方式如下:jsr指令在跳转之前将后面指令(索引7处的return)的地址压入操作数栈。作为跳转目标的astore_2指令将操作数栈上的地址存储到局部变量2中。finally块的代码(在本例中是aload_0和invokevirtual指令)运行。假设该代码的执行正常完成,ret指令从局部变量2检索地址并在该地址恢复执行。执行return指令,tryFinally正常返回。
带有finally子句的try语句被编译为有一个特殊的异常处理器,该处理器可以处理try语句内抛出的任何异常。如果tryItOut抛出异常,则搜索tryFinally的异常表以寻找合适的异常处理器。找到特殊处理器,导致执行在索引8处继续。索引8处的astore_1指令将抛出的值存储到局部变量1中。接下来的jsr指令对finally块的代码进行子程序调用。假设该代码正常返回,索引12处的aload_1指令将抛出的值重新压入操作数栈,接下来的athrow指令重新抛出该值。
同时具有catch子句和finally子句的try语句的编译更复杂:
java
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处的finally块被执行,控制返回到索引19处的return指令,tryCatchFinally正常返回。
如果tryItOut抛出TestExc的实例,异常表中选择第一个(最内层)适用的异常处理器来处理该异常。该异常处理器的代码从索引7开始,将抛出的值传递给handleExc,并在返回时像正常情况一样对索引26处的finally块进行相同的子程序调用。如果handleExc没有抛出异常,tryCatchFinally正常返回。
如果tryItOut抛出的值不是TestExc的实例,或者如果handleExc本身抛出异常,则由异常表中的第二个条目处理,该条目处理索引0到16之间抛出的任何值。该异常处理器将控制转移到索引20,在那里抛出的值首先被存储到局部变量1中。索引26处的finally块代码作为子程序被调用。如果它返回,则从局部变量1检索抛出的值并使用athrow指令重新抛出。如果在finally子句执行期间抛出了新值,则finally子句中止,tryCatchFinally突然返回,将新值抛出给其调用者。
3.14 同步
Java虚拟机中的同步通过监视器进入和退出来实现,要么显式地(使用monitorenter和monitorexit指令),要么隐式地(通过方法调用和返回指令)。
对于用Java编程语言编写的代码,最常见的同步形式也许是synchronized方法。同步方法通常不是使用monitorenter和monitorexit来实现的。相反,它只是在运行时常量池中通过ACC_SYNCHRONIZED标志来区分,该标志由方法调用指令检查(§2.11.10)。
monitorenter和monitorexit指令支持synchronized语句的编译。例如:
java
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指令,都已执行了monitorexit指令。无论方法调用是正常完成(§2.6.4)还是突然完成(§2.6.5),都是如此。为了在方法调用突然完成时强制monitorenter和monitorexit指令的正确配对,编译器生成了将匹配任何异常的异常处理器(§2.10),其关联代码执行必要的monitorexit指令。
3.15 注解
注解在类文件中的表示在§4.7.16–§4.7.22中描述。这些章节清楚地说明了如何在类、接口、字段、方法、方法参数和类型参数的声明上表示注解,以及在这些声明中使用的类型上的注解。包声明上的注解需要额外的规则,在此给出。
当编译器遇到一个需要在运行时提供的带注解的包声明时,它会发出一个具有以下属性的类文件:
- 该类文件表示一个接口,即
ClassFile结构的ACC_INTERFACE和ACC_ABSTRACT标志被设置(§4.1)。 - 如果类文件版本号小于50.0,则
ACC_SYNTHETIC标志未设置;如果类文件版本号为50.0或以上,则ACC_SYNTHETIC标志被设置。 - 该接口具有包访问权限(JLS §6.6.1)。
接口的名称是package-name.package-info的内部形式(§4.2.1)。该接口没有超接口。该接口的唯一成员是《Java语言规范(Java SE 17版)》(JLS §9.2)所隐含的那些成员。包声明上的注解作为RuntimeVisibleAnnotations和RuntimeInvisibleAnnotations属性存储在ClassFile结构的属性表中。
3.16 模块
包含模块声明(JLS §7.7)的编译单元被编译为一个包含Module属性的类文件。
按照惯例,包含模块声明的编译单元的名称是module-info.java,呼应了包含仅包声明的编译单元的package-info.java约定。因此,按照惯例,模块声明的编译形式的名称是module-info.class。
ClassFile结构的access_flags项中的一个标志ACC_MODULE(0x8000)指示该类文件声明了一个模块。ACC_MODULE与ACC_ANNOTATION(0x2000)和ACC_ENUM(0x4000)类似,用于标记该类文件为“不是普通类”。ACC_MODULE不描述类或接口的可访问性。
Module属性明确说明了模块的依赖关系;在ClassFile层面没有隐式的requires指令。如果requires_count项为零,则Java SE平台不会推断requires表或其任何特定条目的存在。java.base是唯一允许requires_count为零的模块,因为它是原始模块。对于其他每个模块,Module属性必须有一个至少长度为1的requires表,因为其他每个模块都依赖于java.base。如果编译单元包含的模块声明(java.base除外)没有明确说明其对java.base的依赖,则编译器必须在requires表中为java.base发出一个条目,并将其标记为ACC_MANDATED以表示它是隐式声明的。
为了封装,Module属性明确说明了普通模块导出和开放的包;在ClassFile层面,普通模块没有隐式的exports或opens指令。如果exports_count项或opens_count项为零,则Java SE平台不会推断exports表或opens表或其任何特定条目的存在。另一方面,对于开放模块,Module属性隐含地说明了模块开放的包。开放模块的所有包都向所有其他模块开放,即使opens_count项为零。
Module属性明确说明了模块对服务的使用和提供;在ClassFile层面没有隐式的uses或provides指令。