首页 > 代码库 > 《深入Java虚拟机学习笔记》- 第18章 finally子句

《深入Java虚拟机学习笔记》- 第18章 finally子句

    本章主要介绍字节码实现的finally子句。包括相关指令以及这些指令的使用方式。此外,本章还介绍了Java源代码中finally子句所展示的一些令人惊讶的特性,并从字节码角度对这些特征进行了解释。

1、微型子例程

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

Java方法与微型子例程使用不同的指令集。跳转到微型子例程的指令是jsr或者jsr_w,将返回地址压入栈。执行完毕后调用ret指令。ret指令并不会从栈中弹出返回地址,而是在子例程开始的时候将返回地址从栈顶取出存储在局部变量,ret指令从局部变量中取出。这是因为finally子句本身会抛出异常或者含有return、break、continue等语句。finally确保会执行到,即使try或者catch中有return等语句。

 

 先看看下面的一道面试题:

int normal(){
   try{
       return 10;
   }finally{
       return 20;
   }
}
以上函数的返回结果时什么?
对于finally,我们通常的认识一般如下:
不管异常是否发生,它都会执行,但是看上面的例子,哪个return先执行?
其实际语义具体描述如下:
try中抛出异常后,如果存在捕获异常的过程,那么这个过程执行完后会执行finally;
try中没有异常发生,那么当try执行到结尾后,执行finally.
注意“try执行到结尾”是指在return之前
 
所以normal()函数实际上返回的是20.
 
Java虚拟机规范中谈到,finally子句类似于一种微型子例程,我们可以通过查看class文件的字节码理解。
 
新建TryFinallyTest.java进行测试:
public class TryFinallyTest{
 
 private static int normal(){
    try{
        return 10;
    }finally{
        return 20;
    }
 }
 
 public static void main(String args[]){
    System.out.println(normal());
 }
 
}
使用javap -c -private TryFinallyTest输出类汇编版本:
(关于以下的jvm指令的含义,可以参考《Java虚拟机规范》或者《Inside JVM》)
Compiled from "TryFinallyTest.java"
public class TryFinallyTest extends java.lang.Object{
public TryFinallyTest();
  Code:
   0:aload_0
   1:invokespecial#1; //Method java/lang/Object."<init>":()V
   4:return
 
private static int normal();
  Code:
   0:bipush10 
   2:istore_1
   3:bipush20
   5:ireturn
   6:astore_2
   7:bipush20
   9:ireturn
  Exception table:
   from   to  target type
     0     3     6   any
     6     7     6   any
//0-2 对应try子句,“3-5”及"7-9"表示finally子例程,而6表示编译器生成的catch处理过程的开端(这可以通过加粗的异常表看到)。
//注意我们没有看到关于return 10的任何信息,该语句已经被忽略了。
 
public static void main(java.lang.String[]);
  Code:
   0:getstatic#2; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:invokestatic#3; //Method normal:()I
   6:invokevirtual#4; //Method java/io/PrintStream.println:(I)V
   9:return
 
}
示例2:Surprise.java
class Surprise {
    static boolean surpriseTheProgrammer(boolean bVal) {
        while (bVal) {
            try {
                return true;
            } finally {
                break;
            }
        }
        return false;
    }
}
相信经过例子1可以知道答案了。
对应的类汇编版本:
Compiled from "Surprise.java"
class Surprise extends java.lang.Object{
Surprise();
  Code:
   0:   aload_0
   1:   invokespecial   #8; //Method java/lang/Object."<init>":()V
   4:   return

static boolean surpriseTheProgrammer(boolean);
  Code:
   0:   iload_0
   1:   ifeq    8
   4:   goto    8
   7:   pop
   8:   iconst_0
   9:   ireturn
  Exception table:
   from   to  target type
     4     7     7   any

}
《Inside JVM》中提到finally子句往往通过jsr指令跳转,但是javap显示的结果并不是这样的,编译器做了一定的优化。
 
示例3:
Finally子句的一个令人困惑的特性:
    private static int normal() {
        int a;
        try {
            a = 1;
            System.out.println("in try:set a to " + a);
            return a;
        } finally {
            a = 2;
            System.out.println("in finally:set a to " + a);
        }
    }
请问normal的返回值是多少,结果如下
in try:set a to 1
in finally:set a to 2
1
先看看javap的结果吧,
private static int normal();
  Code:
   0:iconst_1
   1:istore_0   
//对应a=1(现在栈中压入常量1,再弹出存储到局部变量_0即a中)
   2:getstatic#2; //Field java/lang/System.out:Ljava/io/PrintStream;
   5:new#3; //class java/lang/StringBuilder
   8:dup
   9:invokespecial#4; //Method java/lang/StringBuilder."<init>":()V
   12:ldc#5; //String in try:set a to 
   14:invokevirtual#6; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   17:iload_0
   18:invokevirtual#7; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   21:invokevirtual#8; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   24:invokevirtual#9; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
//9-24对应try块中的打印语句
   27:iload_0
   28:istore_1
//令人困惑的原因在这里-----编译器在try块中把变量a的值存在了标号为1的局部变量中,再跳到语句56我们就明白了,try中返回的是标号1的局部变量的值,而不是修改过的值。
   29:iconst_2
   30:istore_0
   31:getstatic#2; //Field java/lang/System.out:Ljava/io/PrintStream;
   34:new#3; //class java/lang/StringBuilder
   37:dup
   38:invokespecial#4; //Method java/lang/StringBuilder."<init>":()V
   41:ldc#10; //String in finally:set a to 
   43:invokevirtual#6; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   46:iload_0
   47:invokevirtual#7; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   50:invokevirtual#8; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   53:invokevirtual#9; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
//29-53对应finally块
   56:iload_1
   57:ireturn
//56-57对应try中的return a.
 
   58:astore_2
   59:iconst_2
   60:istore_0
//59-60对应finally中的a=2;
   61:getstatic#2; //Field java/lang/System.out:Ljava/io/PrintStream;
   64:new#3; //class java/lang/StringBuilder
   67:dup
   68:invokespecial#4; //Method java/lang/StringBuilder."<init>":()V
   71:ldc#10; //String in finally:set a to 
   73:invokevirtual#6; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   76:iload_0
   77:invokevirtual#7; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   80:invokevirtual#8; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   83:invokevirtual#9; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   86:aload_2
   87:athrow
  Exception table:
   from   to  target type
     0    29    58   any
    58    59    58   any
假设上面方法修改的是对象的引用,结果也是类似的,两者的差别仅在于字节码中的操作码有所不同。
(一个是iload和istore,一个是astore和aload)
所以,如果在finally中修改了变量,而又需要返回该值,那么必须在finally块中添加返回语句。finally里面对result再赋值也不会影响栈顶的返回值,只会影响result的值。但如果在finally里面也加了return的话,这时候虚拟机栈又会把result的值copy到return的栈顶,这时候返回的就是finally里面重新赋值之后的result的值了