【java_基础深入】从语言规范、字节码指令、jvm编译 理解finally语句块细节

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/chenghan_yang/article/details/102685602

一、finally语句块执行的外在表现

1.1. Gosling 亲自对 finally 的描述

a finally clause is always entered with a reason. That reason may be that the try code finished normally, that it executed a control flow statement such as return, or that an exception was thrown in code executed in the Try block. The reason is remembered when the finally clause exits by falling out the bottom. However, if the finally block creates its own reason to leave by executing a control flow statement (such as break or return) or by throwing an exception, that reason supersedes the original one, and the original reason is forgotten. For example, consider the following code:

try {
	// … do something …
	return 1;
}  finally {
	return 2;
}

When the Try block executes its return, the finally block is entered with the “reason” of returning the value 1. However, inside the finally block the value 2 is returned, so the initial intention is forgotten. In fact, if any of the other code in the try block had thrown an exception, the result would still be to return 2. If the finally block did not return a value but simply fell out the bottom, the “return the value 1 ″ reason would be remembered and carried out.

1. 2. 根据对Java的了解进行的翻译

进入finally子句的原因如下:

  1. 语句正常执行
  2. 被执行在流程控制语句中
  3. 执行语句抛出异常 产生的原因会被记录到finally语句执行时。

进一步,finally子句因某种情况跳出了当前执行的流程控制语句的情况有:

  1. break
  2. return
  3. 抛出异常

以上三种情况会取代原有的执行语句(try中的return, break, 抛出异常) , 第三部分将完成这个实验

以下程序为例 :

try {
	// … do something …
	return 1;
} finally {
	return 2;
}

当 try 语句块中执行return语句, finally语句会拿到 1 这个值, 然而,finally内的语句 return 2 会在
return 1前执行。最初的return 1 语句会被覆盖。 值得注意的是, 上面的例子程序中:

  1. 在 try 语句中发生任意异常,finally内的语句 return 2始终会执行。
  2. 如果 finally 语句块中没有 返回值一个值, 那么return 1;将正常执行
    返回一个值,是一个笼统的说法,第二部分将说明】

1.3. 例子程序实验

1.3.1 try中无异常, try 和 finally 同时持有return语句

    public static int testFinallyWithReturn() {
        try {
            return  10;
        } finally {
            return  1000;
        }
    }

返回1000

1.3.2 try中有异常, try 和 finally 同时持有return语句

    public static int testFinallyWithExceptionButReturn(int a) {
        try {
       		// ArithmeticException 会被finally的return语句吃掉
            a = a / 0;  
            return  10;
        } finally {
            // 1. return 可以覆盖抛异常
            // 2. throw 语句和return 语句不可同级并列
            return  1000;
        }
    }

不抛出异常,同时返回1000

1.3.3 try中有异常, try 和 finally 同时抛出异常

    public static int testFinallyWithNotReturn(int a) {
        try {
            a = a / 0;  // ArithmeticException 会被finally的return语句吃掉
            return a + 10;
        } finally {
            // 1. return 可以覆盖抛异常
            // 2. throw 语句和return 语句不可同级并列
            throw new RuntimeException("finally语句块中的异常");
        }
    }

Exception in thread “main” java.lang.RuntimeException: finally语句块中的异常

  • 使用更直观的测试用例测试
    public static int testCatchFinallyWithException(int a) {
        try {
            throw new RuntimeException("try语句块中的异常");
            //a = a / 0;
        } finally {
            throw new RuntimeException("finally语句块中的异常");
        }
    }

Exception in thread “main” java.lang.RuntimeException: finally语句块中的异常

  • 再演进,跟预期一样
    public static int testCatchFinallyWithException(int a){
        try {
            throw new RuntimeException("try语句块中的异常");
        } catch (Exception e) {
            // e.printStackTrace();
            throw new RuntimeException("catch语句中的异常");
        } finally {
            throw new RuntimeException("finally语句块中的异常");
        }
    }

Exception in thread “main” java.lang.RuntimeException: finally语句块中的异常

  • 待填的坑:虽然跟预期一样,但是输出堆栈信息,try 和 catch 确实是抛出了异常。
  • 不打印堆栈,就只有finally的异常
    public static int testCatchFinallyWithException(int a){
        try {
            throw new RuntimeException("try语句块中的异常");
        } catch (Exception e) {
            e.printStackTrace();
            try {
                throw new RuntimeException("catch语句中的异常");
            } catch (Exception e2){
                e2.printStackTrace();
            }
        } finally {
            throw new RuntimeException("finally语句块中的异常");
        }
    }

[打印堆栈信息才能看到前两句]
java.lang.RuntimeException: try语句块中的异常
java.lang.RuntimeException: catch语句中的异常
Exception in thread “main” java.lang.RuntimeException: finally语句块中的异常

1.3.4 finally 中执行 continue 和 break

    public static int testFinallyWithContinue(int a) {
        int i = 0;
        while (i < 2) {
            try {
           		// ArithmeticException 会被finally的continue语句吃掉
                a = a / 0;  
            } finally {
                i++;
                continue;
            }
        }
        return i;
    }
    public static int testFinallyWithBreak(int a) {
        int i = 0;
        while (i < 2) {
            try {
                // ArithmeticException 会被finally的break语句吃掉
                a = a / 0;
            } finally {
                break;
            }

        }
        return i;
    }

都不抛异常


二、finally 外在表现的引申

2.1 对 Gasling 描述的补充

经试验,结论与预期相符。其中Gasling的一个说法较为笼统(或是翻译不到位) :

" finally 语句块中 ***返回值一个值***可以覆盖原有的值"

其中 break 和 continue 并没有返回一个值,但是也成功把try语句中的异常给吃掉了
另外一种更好理解的解释是(转载)

“ 其中 return 和 throw 把程序控制权转交给它们的调用者(invoker),而 break 和 continue 的控制权是在当前方法内转移”

2.2 官网《 The Java Tutorials 》印证了这种说法

The finally Block
The finally block always executes when the try block exits. This ensures that the finally block is executed even if an
unexpected exception occurs. But finally is useful for more than just exception handling — it allows the programmer to
avoid having cleanup code accidentally bypassed by a return,continue, or break. Putting cleanup code in a finally block is
always a good practice, even when no exceptions are anticipated.
Note: If the JVM exits while the try or catch code is being executed, then the finally block may not execute. Likewise, if
the thread executing the try or catch code is interrupted or killed, the finally block may not execute even though the
application as a whole continues.

三、究其原因

3.1 编译为字节码指令

public static int getValue() { 
       int i = 1; 
       try { 
            return i; 
       } finally { 
            i++; 
       } 
    } 
}
public static int getValue(); 
 Code: 
  0:    iconst_1 
  1:    istore_0  
  2:    iload_0   
  3:    istore_1  		 // finally相关的指令
  4:    iinc    0, 1    // 诡异的两段相同指令 -> 下面将图解这个问题
  7:    iload_1 
  8:    ireturn 
  9:    astore_2 
  10:   iinc    0, 1   // 诡异的两段相同指令
  13:   aload_2 
  14:   athrow 
 Exception table: 
  from   to  target type 
    2     4     9   any 
    9    10     9   any 
}

getValue() : 1

3.2 图解字节码指令过程

3.2.1 getValue()正常执行过程

在这里插入图片描述

public static int getValue(); 
 Code: 
  0:    iconst_1  // i (操作数)入栈
  1:    istore_0  // index = 0 的 i store 进本地变量表
  2:    iload_0   // index = 0 的 i load 进操作数栈  【此时操作栈为空】
  3:    istore_1  		// index = 1 的 i store 进本地变量表 (拷贝index = 0 的 i)/* finally 相关的字节码指令 */
  4:    iinc    0, 1    // index = 0 的 i 自增1  									/* finally 相关的字节码指令 */
  7:    iload_1 		// index = 1 的 i load 进操作数栈 【此时操作栈元素为1个】
  8:    ireturn 		// index = 1 的 i 从操作数返回给调用者
  9:    astore_2 		// 异常处理相关
  10:   iinc    0, 1  											 					/* finally 相关的字节码指令 */
  13:   aload_2 		// 异常处理相关
  14:   athrow 			// 异常处理相关
 Exception table: 
  from   to  target type 
    2     4     9   any 
    9    10     9   any 
}

能够清晰得看出: finally 语句块相关的字节码指令被插入到了return 语句之前
finally 语句块中操作的 i 是 拷贝了 index = 0 的 i 生成 index = 1 的 i 到本地变量表
linc 0,1 说明 自增操作在 index = 0 的 i上 也就是 try 语句中的 i, iload_1 把 index = 0 的 i 压入操作数栈中
ireturn 中 ***return 了 i操作数栈的栈顶元素 index = 1 的 i *** , 值自然就是1了

总得来说: finally 拷贝了一份X(i) 变量 生成为Y(i), 对X(i++),返回的却是Y(i)

3.2.2 getValue()异常执行过程
 Exception table: 
  from   to  target type 
    2     4     9   any  // 如果从 2 到 4 这段指令出现异常,则由从 9 开始的指令来处理。
    9    10     9   any 

在这里插入图片描述

3.2.3 getValue() 的 finally 中加入return
  public static int getValue() {
        int i = 1;
        try {
            return i;
        } finally {
            i++;
            return i;
        }
    }

jdk1.8编译后的两点变化
左:加了return | 右 :未加return
在这里插入图片描述

加了return 后 小结
  1. 加了return iload的字节码指令改变,也就改变了最终返回的不是副本
  2. 异常处理中的athrow变成了ireturn,另一方面解释了在try语句块中的异常会给finally中的return吃掉
    所以, 以上代码返回 2

3.2.3 getValue() 的 finally 的 return 前再加入其他代码
  • 只加在finally
    和预期相同,try语句块中同步了finally内新增的代码。
    在这里插入图片描述
  • 加了同名的变量声明
    • 代码对应字节码
      在这里插入图片描述
  • 【重点一】关注第8 9 10 12行
 8 iload_1
 9 istore_3
 10 bipush	34 // push一个常数34到操作数栈中
 12 istore	4  // 把34填到slot = 4 的本地变量表,显然是新增了一个变量

在这里插入图片描述
【重点二】返回前,load了finally创建的变量,也就是操作副本,返回副本

17 iload	4
19 ireturn 

在这里插入图片描述

  • 进一步可以发现,如果finally内创建了try中的同名变量q, 后面代码操作的也是finally 在本地方法表新建的q变量
  • 进一步证明了同名变量 + return 的情况下 : 操作副本,返回副本;
字节码指令 总结
  1. finally 内不加return,操作try中的变量
    1.1 拷贝一份finally中的代码,插入try return之前
    1.2 先拷贝try中的变量X到本地变量表,成为Y,再对X操作。
    1.3 【关键】返回的是Y,也就是操作了X但是不返回,还是返回了操作前的值Y
    总结:finally内不加return,返回的是finally创建的副本,而finally的所有操作都在原来的值上

  1. finally 内return,操作try中的变量
    2.1 拷贝一份finally中的代码,插入try return之前
    2.2 操作的原理同1.2
    2.3 【关键】load了X,再ireturn。也就不是返回finally创建的副本,是实实在在返回了被操作的X

  1. finally 中return,并声明和try中同名的变量,操作变量
    3.1 【关键】load 并store了try中同名的变量到【新的slot】,操作的是新的slot,也就是新的副本
    同样是做了拷贝,只是使用的字节码不一样
    3.2 原来的ireturn是不变
    3.3 把异常处理中的 throw 修改成了 return
    3.4 【关键】load了【新的slot】到操作数栈中,也就是新的副本,最后返回副本
    总结:在finally中声明了try中已有的变量,会针对同名变量的值拷贝一个副本,所有的操作都是针对这个副本。
拓展
没有 catch 语句,哪来的异常处理呢?(转载)

其实,即使没有 catch 语句,Java 编译器编译出的字节码中还是有默认的异常处理的,别忘了,除了需要捕获的异常,还可能有不需捕获的异常(如:RunTimeException 和 Error)。
当从 2 到 4 这段指令出现异常时,将会产生一个 exception 对象,并且把它压入当前操作数栈的栈顶。接下来是 astore_2 这条指令,它负责把 exception 对象保存到本地变量表中 2 的位置,然后执行 finally 语句块,待 finally 语句块执行完毕后,再由 aload_2 这条指令把预先存储的 exception 对象恢复到操作数栈中,最后由athrow指令将其返回给该方法的调用者(main)。

  • 分析异常处理部分
 Exception table: 
  from   to  target type 
    2     4     9   any 
    9    10     9   any 
 Code: 
  0:    iconst_1  
  1:    istore_0  
  2:    iload_0   		// 2, 3 ,4 任意发生异常, iload_1 和 ireturn 都不执行, 
  						// 也就无法返回 index = 0 的 i, 会跳到9开始,最终到14抛出
  3:    istore_1  		/* finally 在try中插入的字节码指令 */   
  4:    iinc    0, 1    /* finally 在try中插入的字节码指令 */ 
  7:    iload_1 		
  8:    ireturn 		
  9:    astore_2 		// 异常处理相关
  10:   iinc    0, 1  	/* finally 预期内的字节码指令 */
  13:   aload_2 		// 异常处理相关
  14:   athrow 			// 异常处理相关

复盘

也就解释了为什么try 语句中出现的异常 会给 finally 中的 return 、 break 、 continue 吃掉。
也解释了 finally 最终return 的值会在 try return 之前,也就是覆盖了 try 中的return
当然,try 中如果是个函数 如 return function(); 相当于是 int temp = function(); return temp;
function() 会执行,但是temp的值无法返回

辅助材料: finally 相关的编译底层实现

《 The JavaTM Virtual Machine Specification, Second Edition 》中 7.13 节 Compiling finally中说道(转载的翻译)

实际上,Java 虚拟机会把 finally 语句块作为 subroutine(对于这个 subroutine 不知该如何翻译为好,干脆就不翻译了,免得产生歧义和误解。)直接插入到 try 语句块或者 catch 语句块的控制转移语句之前。但是,还有另外一个不可忽视的因素,那就是在执行 subroutine(也就是 finally 语句块)之前,try 或者 catch 语句块会保留其返回值到本地变量表(Local Variable Table)中。待 subroutine 执行完毕之后,再恢复保留的返回值到操作数栈中,然后通过 return 或者 throw 语句将其返回给该方法的调用者(invoker)。请注意,前文中我们曾经提到过 return、throw 和 break、continue 的区别,对于这条规则(保留返回值),只适用于 return 和 throw 语句,不适用于 break 和 continue 语句,因为它们根本就没有返回值。

以上就能解释字节码指令的 生成逻辑


参考资料:https://www.ibm.com/developerworks/cn/java/j-lo-finally/

注:文中的图片和部分翻译均转载上述连接

猜你喜欢

转载自blog.csdn.net/chenghan_yang/article/details/102685602