第18章 finally子句

18.1微型子例程

字节码中的finally子句在方法内部的表现很像“微型子例程”。Java虚拟机在每个try语句块和与其相关的catch子句的结尾处都会“调用”finally子句的子例程。finally子句结束后(这里的结束指的是finally子句中最后一条语句正常执行完毕,不包括抛出异常,或执行return、comimie、 break等情况),隶属于这个finally子句的微型子例程执行“返回”操作。程序在第一次调用微型子例程的地方继续执行后面的语句。

jsr指令是使Java虚拟机跳转到微型子例程的操作码。jsr指令使用一个双字节长度的操作数, 这个操作数指出从jsr指令处到微型子例程开始处的16位带符号的偏移量。另外一条指令是jsr_w, 它与jsr完成同样的功能,但是它支持更长的操作数(4个字节长)。当Java虚拟机遇到jsr或者jsr_w 指令,它会把返回地址压人桟,然后从微型子例程的开始处继续执行。返回地址是紧接在jsr或jsr_w 操作码和操作数后字节码的地址(偏移量或者本地指针)。该地址的类型为returnAddress。

微型子例程执行完毕后,将调用ret指令,ret指令的功能是执行从子例程中返回的操作。ret 指令只有一个操作数,这个操作数是一个存储返回地址的局部变量的索引。表18-1中总结了处理finally子句的操作码。

不要混淆微型子例程与Java方法。Java方法与微型子例程使用不同的指令集。例如,调用 Java方法可以使用invokevirtual和invokespecial等指令,使Java方法返回可以使用return,areturn, ireturn等指令。

jsr指令并不会调用Java方法,它只能跳转到相同方法中不同的操作码处。同样,ret指令也不能令Java方法返回,它只能使虚拟机跳回相同方法中调用jsr操作码和它的操作数之后的位置。 本书中,实现finally子句的字节码被称为“微型子例程”,因为它们在一个方法的字节码流中的 表现如同一个小子例程一样。

18.2不对称的调用和返回

你也许会认为,ret指令应当从栈中弹出返回地址,因为返回地址也已被jsr指令压人栈。不是这样的,ret指令并不会这样做。在每一个子例程的开始处,返回地址都从栈顶端弹出,并且存储在局部变量中,稍后,ret指令将会从这个局部变量中取出返回地址。这种对返回地址的不对称的工作方式是必要的,因为finally子句(微型子例程)本身会抛出异常或者含有return、 break、continue等语句。由于这些可能性的存在,这个被jsr指令压入栈的额外返回地址必须立即从栈中移除,因此,当finally子句通过break、continue、return或者抛出异常退出时,这个问 題就不必再考虑了。

例如,下面的代码包含了一个通过break语句退出的finally子句。执行这段代码的结果是, 无论给方法surpriseTheProgrammer ()的参数bVal传入什么,该方法都将返回false。
// On CD-ROM in file opcodes/ex3/Surprise.java
class Surprise {

static boolean surpriseTheProgrammer(boolean bVal) {
while (bVal) {
try {
return true;
}
finally {
break;
}
}
return false;
}
}

上面的例子指出了返回地址必须在finally子句开始之处存入局部变量中的原因。因为finally子句在break语句处退出,它绝不会执行ret指令。因此,Java虚拟机不会执行返回true的语句。当 它执行完break语句后,会退出至while语句的终结处,即闭括号处。下一条语句将会返回false, 这个推断与Java虚拟机的执行过程完全相同。

扫描二维码关注公众号,回复: 8063755 查看本文章

无论使用break、return、continue,还是通过抛出异常退出finally子句,所显示出的特性都是相同的。在这些例子中,finally子句结尾处的ret指令永远不会执行到。正因为它永远不会被执行到,就无法指望它从栈中除去返回地址。因此,虚拟机在finally子句开始的地方就将返回地址存储到局部变量中。

下面所列出的方法是finally子句的一个完全范例,它包含了一个try语句块,这个try语句块中有两个退出点。在这个例子里,两个退出点都是return语句。
// On CD-ROM in file opcodes/ex1/Nostalgia.java
class Nostalgia {

static int giveMeThatOldFashionedBoolean(boolean bVal) {
try {
if (bVal) {
return 1;
}
return 0;
}
finally {
System.out.println("Got old fashioned.");
}
}
}
方法giveMeThatOldFashionedBoolean()被编译为如下的字节码:
//The bytecode sequence for the try block:
0 iload_0 // Push local variable 0 (bval parameter)
1 ifeq 11 // Pop int, if equal to 0, jump to 11 (just // past the if statement):if(bval){}
4 iconst_1 // Push int 1
// Pop an int (the 1), store into local
5 istore_1 // variable 1
// Jump to the Mini-subroutine for the
6 jsr 24 // finally clause
9 iload_1 // Push local variable 1 (the 1)
// Return int on top of the stack (the 1):
10 ireturn // return 1;
11 iconst_0 // Push int 0
// Pop an int (the 0), store into local
12 istore_1 // variable 1
// jump to the mini-subroutine for the
13 jsr 24 // finally clause
16 iload_l // Push local variable 1 (the 0)
// Return int on top of the stack (the 0):

17 ireturn // return 0;
// the bytecode sequence for a catch clause that catches
// any kind of exception thrown from within the try block.
// Pop the reference to the thrown exception,
18 astore_2 // store into local variable 2
//Jump to the mini-subroutine for the
19 jsr 24 // finally clause
// Push the reference (to the thrown
22 aload_2 // exception) from local variable 2
23 athrow // Rethrow the same exception

// The miniature subroutine that implements the finally
// block.
// Pop the return address, store it in local
24 astore_3 // variable 3
// Get a reference to java.lang.System.out

25 getstatic #7 <Field java.io.PrintStream out>
// Puah reference to "Got old fashioned."
// String from the constant pool
28 ldc #1 <String "Got old fashioned.">
//Invoke System.out.printIn()

30 invokevirtual #8
<Method void printIn(java.lang.String)>
// Return to return address stored in local
33 ret 3 // variable 3

try语句块的字节码中包含了两条jsr指令,此外,catch子句里还有一条jsr指令。如果在执行try语句块过程中抛出了一个异常,最后的语句块必须接着执行。因此,编译器在字节码中加入了catch子句。所以这里的catch子句只调用了描述finally子句的微型子例程,然后再抛出一个同 样的异常。如下所列的giveMeThatOldFashionedBoolean ()方法的异常表指出:凡是在地址0到17(含)之间(即所有实现try语句块的字节码中)抛出的异常,都被从地址18开始的catch子句处理。
Exception table:
from to target type
0 18 18 any
开始执行finally子句时,返回地址被弹出桟并被保存在局部变量3中。在finally子句结束的时候,ret指令再从局部变量3中取得返回地址。
由javac产生的hopAround()方法的字节码如下所示:
Exception table:
from to target type
2 4 10 any
2 31 31 any
hopAround ()方法在第一条finally子句中通过执行到闭括号返回,而它在第二条finally子句中是通过执行一条continue语句返回。第一条finally子句通过它的ret指令退出。而由于第二条finally子句使用continue退出,这样就不会执行它的ret指令。continue语句使Java虚拟机跳转至while循环的开始处。尽管方法中有return语句,但执行这条return语句前,将会首先执行第二条 finally子句,于是得出结论:这个循环是一个死循环。在finally子句中的continue语句取代了 return语句,这个方法永远无法返回。

需要注意的是,实现return语句的字节码在跳转到实现第二条finally子句的微型子例程的时, 已经把返回值存入到局部变量1中。在微型子例程返回后(在上述情况下,它永远无法返回,因 为总是在返回之前执行continue语句),再从局部变量1中取出返回值,并且返回。

这个过程强调了Java虚拟机在finally子句执行完毕前返回一个值的方式。尽管i的值是在执行完finally子句后返回的,但虚拟机仍将返回执行finally子句前i的值。所以就算finally子句会改变i的值,该方法仍将返回finally子句未执行前i的值。如果需要使用finally子句来改变方法返回值, 就不得不在finally子句中再加入一条return语句,用来返回被finally子句更新过的返回值。

猜你喜欢

转载自www.cnblogs.com/mongotea/p/11980086.html