分析和例子来源于《深入java虚拟机》一书。
finally语句一定会执行吗?
答案是不一定。
虽然很多地方都强调一些释放资源的方法要在finally
语句块中执行,因为finally
语句块一定会执行,但是其实也是有条件的。
finally
语句不执行的两个条件:
在执行
try{}catch
语句之前已经退出程序在
try{}catch
语句中执行了System.exit()
语句导致JVM退出并停止工作
只要是程序到达了try{}catch
语句块,除非JVM停止工作,否则都会执行finally
语句。那如果try{}catch
语句或者finally
语句中含有return
语句,会不会结束程序呢?如果不,那又是怎样工作的呢?
finally子句的调用过程
首先看表面,JVM在每个try
语句块和与其相关的catch
子句的结尾处都会“调用”finally
子句,finally
子句结束后(这里结束指的是finally
子句的最后一条语句正常执行完毕,不包括抛出异常,或者执行return、continue、break
等情况),执行返回操作,程序在第一次调用finally
语句的地方继续执行后面的语句。
然后介绍子例程,子例程就是主程序中的一段代码,在主程序中可以调用,执行特定的功能同时与主程序相对独立。书中将finally
子句在方法内部的行为看作是一个“微型子例程”,执行用户设置的功能同时与外部程序相对独立。
接着介绍原理,当程序需要执行finally
子例程的时候,会先将返回地址压入到栈中,然后从子例程的开始处继续执行。子例程执行完毕后,将调用ret指令,该指令的功能是执行子例程返回的操作,该指令中只有一个操作数,保存的是返回地址的局部变量的索引。
这里可能会疑惑,为什么前面将返回地址压入栈,而这里ret
指令保存的是局部变量的索引,不应该是从栈中弹出返回地址,执行返回操作吗?这里就涉及到了return
等命令。
不对称的调用和返回
不对称指的是前面压入栈,后面不通过出栈返回值。实际上,在每个子例程开始处,返回地址都从栈顶端弹出,并且保存在局部变量中。稍后的ret
指令会从这个局部变量中取出返回地址。
这样的不对称调用是很必要的,因为finally
子句本身会抛出异常或者含有return、break、continue
等语句,如果在执行finally
子句时遇到了退出语句,可能会导致栈帧被弹出或栈被清空,如果返回地址还被压入在栈中,会被清除,换句话说也就是finally
无法正常执行完,ret
指令也就不会调用,返回地址也就无法拿到,程序就无法从跳转的地方继续执行。所以需要保存在局部变量中。
下面代码包含了一个通过break语句退出的finally
子句,执行的结果是,无论参数传入什么,该方法都将返回false
。
public static boolean test(boolean value){
while(value){
try{
return true;
}
finally{
break;
}
}
return false;
}
虽然try
语句中让方法返回了,但是因为后面紧跟着finally
子句,所以将while
语句终结处的地址压入栈中,然后开始子例程,在子例程一开始的时候,就将地址所在的栈帧弹出并将返回地址赋值给一个局部变量,然后执行break
语句退出while
循环,最后返回false
。也就是说return
在此例中并没有起到作用。
实际上,return
还是执行了操作的,return
将返回的值保存到一个局部变量中,如果后面finally
子句中执行退出语句,那么此返回值无效;如果没有退出,分两种情况,一种是在finally
子句中改变了需要返回的变量的值,但这种操作并不会影响前面已经保存了的返回值,程序依然会返回局部变量中的值,另一种情况是想要在finally
子句中改变返回值,那么就必须在finally
中加入return
语句,用来返回被finally
子句更新过的返回值。
上面的解释也可以说明为什么finally
子句被称为“微型子例程”,因为它的运行相当独立,首先程序很大的几率会执行此语句,此外它的执行与主程序关系不大,逻辑上的联系不多,更重要的是,它对于主程序中的变量做出的改变大多没有落到实处,也就是没有起到作用,像是return
语句,所以子例程很恰当。