Java的JVM编译介绍

介绍Java的虚拟机的基本知识

Posted by qin4zhang on August 25, 2019

注意

想法及时记录,实现可以待做。

要说明什么是JVM,无疑看官方文档是最好的方式,看看JVM规范。官方文档

JVM介绍

Java虚拟机是Java平台的基石。它是负责其硬件和操作系统独立性、编译代码的小尺寸以及保护用户免受恶意程序的能力的技术组成部分。

Java虚拟机对Java编程语言一无所知,只知道一种特殊的二进制格式,即类文件格式。类文件包含Java虚拟机指令(或字节码)和符号表以及其他辅助信息。

为了安全起见,Java虚拟机对类文件中的代码施加了很强的语法和结构约束。但是,任何具有可以以有效类文件表达的功能的语言都可以由Java虚拟机托管。

对于JDK、JRE、JVM的关系,也说明下。JRE包括了JVM和Java所需的一些类库等。JDK除了包含JRE之外,还包括了一些开发、诊断工具。

一份Java源码,编译成字节码文件(class文件)后,通过JVM即可在各个操作系统中运行,由JVM负责与操作系统交互,而不是由具体的语言,这也是”Write Once, Run Anywhere”。

其实不仅对于Java这一种语言,只要能通过编译器编译成符合规范的字节码文件,任何语言都是可以运行在JVM上,常见的语言还有Scala、Groovy等等。

JVM编译

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

请注意,术语“编译器”有时用于指从Java虚拟机指令集到特定CPU指令集的翻译程序。这种转换器的一个例子是即时(JIT)代码生成器,它仅在装入Java虚拟机代码之后才生成特定于平台的指令。本章不讨论与代码生成相关的问题,只讨论与将用Java编程语言编写的源代码编译成Java虚拟机指令相关的问题。

  1. 编译格式

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

<index> <opcode> [ <operand1> [ <operand2>... ]] [<comment>]
是指令的操作码在包含此方法的Java虚拟机代码字节的数组中的索引。或者,可以被认为是方法开始处的字节偏移量。 是指令操作码的助记符,而0个或多个是指令的操作数。可选的是以行尾注释语法给出的: ``` 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 ``` 2. 常量、局部变量和控制结构的使用 Java虚拟机代码展示了一组由Java虚拟机类型的设计和使用强加的通用特征。在第一个例子中,我们遇到了许多这样的例子,我们对它们进行了一些详细的考虑。 spin方法简单地围绕一个空的for循环旋转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虚拟机当前帧的操作数堆栈中获取一个或多个操作数,或者将结果推回到操作数堆栈中。 每次调用方法时都会创建一个新的帧,并随之创建一个新的操作数堆栈和一组局部变量,供该方法使用。 因此,在计算的任何一点上,每个控制线程可能有许多帧和同样多的操作数堆栈,对应于许多嵌套的方法调用。只有当前帧中的操作数堆栈是活动的。 Java虚拟机的指令集通过对其不同数据类型的操作使用不同的字节码来区分操作数类型。 spin方法只对int类型的值进行操作。其编译代码中用于操作类型化数据的指令(iconst_0, istore_1, iinc, iload_1, if_icmplt)都专门用于int类型。 spin中的两个常量0和100使用两个不同的指令压入操作数堆栈。0是用iconst_0指令推送的,这是iconst_指令族中的一个。100是使用bipush指令推入的,该指令将作为立即操作数来获取它推入的值。 Java虚拟机经常利用某些操作数(在iconst_指令的情况下,int常量-1、0、1、2、3、4和5)的可能性,使这些操作数隐式地存在于操作码中。 因为iconst_0指令知道它将下入一个int 0,所以iconst_0不需要存储操作数来告诉它下入的值是什么,也不需要获取或解码操作数。 将0的push编译为bipush 0是正确的,但是会使编译后的代码延长一个字节。一个简单的虚拟机也会花费额外的时间. 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) ``` 如果spin示例对循环计数器使用了int以外的数据类型,则编译后的代码必然会发生变化以反映不同的数据类型。例如,如果spin示例使用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 ``` 3. 算数 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 ``` 4. 访问运行时常量池 许多数值常量以及对象、字段和方法都是通过当前类的运行时常量池访问的。稍后将考虑对象访问。使用`ldc`、`ldc_w`和`ldc2_w`指令管理int、long、float和double类型的数据,以及对String类实例的引用。 `ldc`和`ldc_w`指令用于访问运行时常量池中double和long以外的类型的值(包括类String的实例)。只有当有大量运行时常量池项且需要更大的索引来访问某个项时,才使用ldc_w指令来代替ldc。ldc2_w指令用于访问所有double和long类型的值;没有非广泛的变体。 字节、char或short类型的整型常量,以及小的int值,可以使用`bipush`、`sipush`或`iconst_`指令进行编译。某些小型浮点常量可以使用`fconst_`和`dconst_`指令进行编译。 在所有这些情况下,编译都很简单。例如常量: ``` 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... ``` 5. 更多的控制的例子 for语句的汇编在前面的章节中已经显示。大多数Java编程语言的其他控制结构(if-then-else、do、while、break和continue)也以明显的方式进行编译。 switch语句的编译在单独的章节中处理,异常的编译和finally子句的编译也是如此。 作为另一个例子,虽然Java虚拟机提供的特定控制传输指令因数据类型而异,但用一种明显的方式编译while循环。通常,对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 ``` 涉及其他数据类型的控制构造以类似的方式编译,但必须使用这些数据类型可用的指令。例如,这将导致代码的效率降低,因为需要更多的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 ``` 每种浮点类型都有两个比较指令:`fcmpl`和`fcmpg`用于float类型,`dcmpl`和`dcmpg`用于double类型。这些变体仅在对NaN的处理上有所不同。 NaN是无序的,所以如果任何一个浮点数都是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 ``` 6. 接收参数 如果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中传递对其实例的引用。在Java编程语言中,可以通过this关键字访问实例。 类(静态)方法没有实例,所以对于它们来说,使用局部变量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开始。 7. 调用方法 实例方法的普通方法调用根据对象的运行时类型分派。(用c++术语来说,它们是虚的。)这样的调用是使用`invokvirtual`指令实现的,该指令以一个运行时常量池项的索引作为参数,该索引给出了对象的类类型的二进制名称、要调用的方法的名称以及该方法的描述符的内部形式。要调用前面定义为实例方法的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() ``` 调用addTwoStatic (addTwo的类(静态)变体)是类似的,如下所示: ``` 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 ``` 编译类(静态)方法的调用非常类似于编译实例方法的调用,只不过这不是由调用者传递的。因此,方法参数将从局部变量0开始接收。`invokestatic`指令总是用来调用类方法。 必须使用invoke特殊指令来调用实例初始化方法。在调用超类(super)中的方法和调用私有方法时也会使用它。例如,给定类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 ``` 注意,使用`invokspecial`指令调用的方法总是将此参数作为其第一个参数传递给被调用的方法。通常,它是在局部变量0中接收的。 要调用方法句柄的目标,编译器必须形成一个记录实际参数和返回类型的方法描述符。编译器不能对实参执行方法调用转换;相反,它必须根据它们自己的未转换类型将它们压入堆栈。 编译器将方法句柄对象的引用按惯例安排在实参之前压入堆栈。编译器发出一个调用虚指令,该指令引用描述参数和返回类型的描述符。 通过方法解析的特殊安排,一个调用invokeExact或调用`java.lang.invoke.MethodHandle`方法的`invokevirinstruction`将总是链接,只要方法描述符语法正确,并且描述符中命名的类型可以被解析。 8. 使用类实例 Java虚拟机类实例是使用Java虚拟机的新指令创建的。回想一下,在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.()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.()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定义如下: ``` 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)的操作数不是类实例中字段的偏移量。 编译器生成对实例字段的符号引用,这些字段存储在运行时常量池中。这些运行时常量池项将在运行时解析,以确定被引用对象中的字段的位置。 9. 数组 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.()V 17 aastore // Store new Thread in array at 0 18 return ``` `anewarray`指令也可以用来创建多维数组的第一个维度。或者,`multianewarray`指令可以用于一次创建多个维度。例如,三维数组: ``` 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 areturn ``` `multianewarray`指令的第一个操作数是要创建的数组类类型的运行时常量池索引。第二个是要实际创建的数组类型的维数。 `multianewarray`指令可以用来创建该类型的所有维度,create3DArray的代码显示。请注意,多维数组只是一个对象,因此分别由aload_1和return指令加载和返回。 所有数组都有关联的长度,可以通过`arraylength`指令访问。 10. 编译switch语句 `switch`语句的编译使用`tableswitch`和`lookupswitch`指令。当switch的情况可以有效地表示为目标偏移量表的索引时,就使用`tableswitch`指令。 如果switch表达式的值不在有效索引的范围内,则使用switch的default目标。例如: ``` 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虚拟机的`tableswitch`和`lookupswitch`指令只对int数据进行操作。由于对byte、char或short值的操作在内部被提升为int类型,因此如果switch的表达式求值为这些类型之一,则编译时就像求值为int类型一样。 如果chooseNear方法是使用short类型编写的,那么就会生成与使用int类型时相同的Java虚拟机指令。其他数值类型必须缩小为int类型,以便在switch中使用。 在`switch`的情况比较稀疏的情况下,`tableswitch`指令的表表示在空间方面变得低效。可以使用`lookupswitch`指令代替。 `lookupswitch`指令对int键(`case`标签的值)和表中的目标偏移量进行配对。当执行一个`lookupswitch`指令时,switch表达式的值将与表中的键值进行比较。 如果其中一个键与表达式的值匹配,则在相关的目标偏移量处继续执行。如果没有关键字匹配,则在`default`目标继续执行。例如,以下代码已编译: ``` int chooseFar(int i) { switch (i) { case -100: return -1; case 0: return 0; case 100: return 1; default: return -1; } } ``` 编译为: ``` 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虚拟机指定`lookupswitch`指令的表必须按键排序,这样实现可以使用比线性扫描更高效的搜索。 即便如此,`lookupswitch`指令必须搜索它的键来匹配,而不是简单地执行边界检查并索引到表中(如`tableswitch`)。因此,在空间考虑允许选择的情况下,`tableswitch`指令可能比`lookupswitch`更有效。 11. 操作数栈上的操作 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虚拟机从不允许其操作数堆栈操作指令修改或拆分操作数堆栈上的单个值。 12. 抛出和处理异常 使用throw关键字从程序中抛出异常。它的编译很简单: ``` 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.()V 11 athrow // Second reference is thrown 12 return // Never get here if we threw TestExc ``` try-catch构造的编译很简单。例如: ``` 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 TestExc ``` handleExc的调用,即catch子句的内容,也像普通方法调用一样进行编译。然而,catch子句的存在会导致编译器生成一个异常表项。 catchOne方法的异常表有一个条目对应于catchOne的catch子句可以处理的一个参数(类TestExc的实例)。 如果在执行catchOne中索引0和4之间的指令时抛出了TestExc实例的某个值,那么控制将转移到索引5的Java虚拟机代码中,该代码实现了catch子句的块。 如果抛出的值不是TestExc的实例,那么catchOne的catch子句不能处理它。相反,该值被重新抛出给catchOne的调用者。 一个try可以有多个catch子句: ``` 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语句: ``` 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虚拟机不强制异常表项的嵌套或任何排序。 然而,由于try-catch构造是结构化的,因此编译器总是可以对异常处理程序表的条目进行排序,以便对于任何抛出的异常和该方法中的任何程序计数器值,与抛出异常匹配的第一个异常处理程序对应于最内层的匹配catch子句。 例如,如果tryItOut(在索引1处)的调用抛出了TestExc1的实例,则它将由调用handleExc1的catch子句处理。即使异常发生在外部catch子句(捕获TestExc2)的范围内,即使外部catch子句可能已经能够处理抛出的值,情况也是如此。 需要注意的是,catch子句的范围在“from”端是包含的,在“to”端是排他的。因此,catch子句捕获TestExc1的异常表项不包含偏移量4处的返回指令。 但是,catch子句捕获TestExc2的异常表项包含了偏移量11处的返回指令。嵌套catch子句中的返回指令包含在嵌套catch子句涵盖的指令范围内。 13. 编译finally语句 try-finally语句的编译类似于try-catch语句。在将控制转移到try语句之外之前,无论转移是正常的还是突然的,因为已经抛出了异常,都必须首先执行finally子句。对于这个简单的例子: ``` 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处返回)压入操作数堆栈。跳转目标astore_2指令将操作数堆栈上的地址存储到局部变量2中。 最后的代码块(在本例中是`aload_0`和`invokvirtual`指令)将运行。假设该代码的执行正常完成,ret指令从局部变量2中检索地址,并在该地址处继续执行。返回指令被执行,tryFinally正常返回。 带有finally子句的try语句被编译为具有特殊的异常处理程序,该异常处理程序可以处理try语句中抛出的任何异常。 如果tryItOut抛出一个异常,则搜索tryFinally的异常表以寻找合适的异常处理程序。找到了特殊的处理程序,导致在索引8处继续执行。 索引8处的`astore_1`指令将抛出的值存储到局部变量1中。下面的jsr指令对finally块的代码执行子例程调用。假设代码正常返回,第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处的finally块,控制返回索引19处的返回指令,tryCatchFinally正常返回。 如果tryItOut抛出TestExc的一个实例,则会选择异常表中第一个(最内层)适用的异常处理程序来处理该异常。 该异常处理程序的代码从索引7开始,将抛出的值传递给handleExc,并在其返回时对索引26处的finally块进行与正常情况相同的子例程调用。如果handleExc没有抛出异常,则tryCatchFinally正常返回。 如果tryItOut抛出一个不是TestExc实例的值,或者如果handleExc本身抛出一个异常,则该条件由异常表中的第二个条目处理,该条目处理索引0到16之间抛出的任何值。 该异常处理程序将控制转移到索引20,在索引20中,抛出的值首先存储在局部变量1中。索引26处finally块的代码作为子程序调用。 如果返回,则从局部变量1检索被抛出的值,并使用`athrow`指令重新抛出。如果在finally子句执行期间抛出一个新值,finally子句将中止,tryCatchFinally会突然返回,并将新值抛出给调用者。 14. 同步操作 Java虚拟机中的同步是通过监视器的进入和退出实现的,可以是显式的(通过使用`monitorenter`和`monitoreexit`指令),也可以是隐式的(通过方法调用和返回指令)。 对于用Java编程语言编写的代码,可能最常见的同步形式是`synchronized`方法。`synchronized`方法通常不使用`monitorenter`和`monitorexit`来实现。 相反,它只是在运行时常量池中通过`ACC_SYNCHRONIZED`标志进行区分,`ACC_SYNCHRONIZED`标志由方法调用指令检查。 `monitorenter`和`monitorexit`指令可以编译同步语句。例如: ``` 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 ``` 编译器确保在任何方法调用完成时,自方法调用以来执行的每个`monitorerenter`指令都将执行一个`monitorexit`指令。 这就是方法调用是正常完成还是突然完成的情况。为了在突然的方法调用完成时强制对`monitorenter`和`monitorexit`指令进行正确的配对,编译器生成异常处理程序,它将匹配任何异常,其相关代码执行必要的`monitorexit`指令。 15. 注解 当编译器遇到一个必须在运行时可用的带注释的包声明时,它会发出一个带有以下属性的类文件: - 类文件代表一个接口,也就是说,设置了`ClassFile`结构的`ACC_INTERFACE`和`ACC_ABSTRACT`标志 - 如果类文件版本号小于50.0,则`ACC_SYNTHETIC`标志未设置;如果类文件版本号是50.0或更高,则设置`ACC_SYNTHETIC`标志。 - 该接口具有包访问权限 - 接口的名称是package-name.package-info的内部形式 - 接口没有超级接口。 - 该接口的唯一成员是Java语言规范,Java SE 8版所暗示的。 - 包声明上的注释作为`RuntimeVisibleAnnotations`和`RuntimeInvisibleAnnotations`属性存储在`ClassFile`结构的属性表中。 ## 参考 1. 官方文档