final 能提高性能吗?
final 能否提高性能一直是业界争论的点,很多书籍中都介绍了可以在特定场景提高性能,例如 final 可能用于帮助 JVM 将方法进行内联,可以改造编译器进行编译的能力等等,但这些结论很多都是基于假设作出的。
或许 R 大这篇回答会给我们一些结论 https://www.zhihu.com/question/21762917
大致说的就是无论局部变量声明时带不带 final 关键字修饰,对其访问的效率都一样。
比如下面这段代码(不带 final 的版本)
static int foo() { int a = someValueA(); int b = someValueB(); return a + b; // 这里访问局部变量 }
带 final 的版本
static int foo() { final int a = someValueA(); final int b = someValueB(); return a + b; // 这里访问局部变量 }
使用 javac
编译后得出来的结果一摸一样。
invokestatic someValueA:()I istore_0 // 设置a的值 invokestatic someValueB:()I istore_1 // 设置b的值 iload_0 // 读取a的值 iload_1 // 读取b的值 iadd ireturn
因为上面是使用引用类型,所以字节码相同。
如果是常量类型,我们看一下
// 带 final static int foo(){ final int a = 11; final int b = 12; return a + b; } // 不带 final static int foo(){ int a = 11; int b = 12; return a + b; }
我们分别编译一下两个 foo
方法,会发现如下字节码
左边是非 final 关键字修饰的代码,右边是有 final 关键字修饰的代码,对比这两个字节码,可以得出如下结论。
- 不管有没有 final 修饰 ,int a = 11 或者 int a = 12 都当作常量看待。
- 在 return 返回处,不加 final 的 a + b 会当作变量来处理;加 final 修饰的 a + b 会直接当作常量处理。
其实这种层面上的差异只对比较简易的 JVM 影响较大,因为这样的 VM 对解释器的依赖较大,原本 Class 文件里的字节码是怎样的它就怎么执行;对高性能的 JVM(例如 HotSpot、J9 等)则没啥影响。
所以,大部分 final 对性能优化的影响,可以直接忽略,我们使用 final 更多的考量在于其不可变性。
深入理解 finally
我们上面大致聊到了 finally 的使用,其作用就是保证在 try 块中的代码执行完成之后,必然会执行 finally 中的语句。不管 try 块中是否抛出异常。
那么下面我们就来深入认识一下 finally ,以及 finally 的字节码是什么,以及 finally 究竟何时执行的本质。
- 首先我们知道 finally 块只会在 try 块执行的情况下才执行,finally 不会单独存在。
这个不用再过多解释,这是大家都知道的一条规则。finally 必须和 try 块或者 try catch 块一起使用。
- 其次,finally 块在离开 try 块执行完成后或者 try 块未执行完成但是接下来是控制转移语句时(return/continue/break)在控制转移语句之前执行
这一条其实是说明 finally 的执行时机的,我们以 return 为例来看一下是不是这么回事。
如下这段代码
static int mayThrowException(){ try{ return 1; }finally { System.out.println("finally"); } } public static void main(String[] args) { System.out.println(FinallyTest.mayThrowException()); }
从执行结果可以证明是 finally 要先于 return 执行的。
当 finally 有返回值时,会直接返回。不会再去返回 try 或者 catch 中的返回值。
static int mayThrowException(){ try{ return 1; }finally { return 2; } } public static void main(String[] args) { System.out.println(FinallyTest.mayThrowException()); }
- 在执行 finally 语句之前,控制转移语句会将返回值存在本地变量中
看下面这段代码
static int mayThrowException(){ int i = 100; try { return i; }finally { ++i; } } public static void main(String[] args) { System.out.println(FinallyTest.mayThrowException()); }
上面这段代码能够说明 return i 是先于 ++i 执行的,而且 return i 会把 i 的值暂存,和 finally 一起返回。
finally 的本质
下面我们来看一段代码
public static void main(String[] args) { int a1 = 0; try { a1 = 1; }catch (Exception e){ a1 = 2; }finally { a1 = 3; } System.out.println(a1); }
这段代码输出的结果是什么呢?答案是 3,为啥呢?
抱着疑问,我们先来看一下这段代码的字节码
字节码的中文注释我已经给你标出来了,这里需要注意一下下面的 Exception table
,Exception table 是异常表,异常表中每一个条目代表一个异常发生器,异常发生器由 From 指针,To 指针,Target 指针和应该捕获的异常类型构成。
所以上面这段代码的执行路径有三种
- 如果 try 语句块中出现了属于 exception 及其子类的异常,则跳转到 catch 处理
- 如果 try 语句块中出现了不属于 exception 及其子类的异常,则跳转到 finally 处理
- 如果 catch 语句块中新出现了异常,则跳转到 finally 处理
聊到这里,我们还没说 finally 的本质到底是什么,仔细观察一下上面的字节码,你会发现其实 finally 会把 a1 = 3
的字节码 iconst_3 和 istore_1 放在 try 块和 catch 块的后面,所以上面这段代码就形同于
public static void main(String[] args) { int a1 = 0; try { a1 = 1; // finally a1 = 3 }catch (Exception e){ a1 = 2; // finally a1 = 3 }finally { a1 = 3; } System.out.println(a1); }
上面中的 Exception table 是只有 Throwable
的子类 exception 和 error 才会执行异常走查的异常表,正常情况下没有 try 块是没有异常表的,下面来验证一下
public static void main(String[] args) { int a1 = 1; System.out.println(a1); }
比如上面我们使用了一段非常简单的程序来验证,编译后我们来看一下它的字节码
可以看到,果然没有异常表的存在。
finally 一定会执行吗
上面我们讨论的都是 finally 一定会执行的情况,那么 finally 一定会被执行吗?恐怕不是。
除了机房断电、机房爆炸、机房进水、机房被雷劈、强制关机、拔电源之外,还有几种情况能够使 finally 不会执行。
- 调用
System.exit
方法 - 调用
Runtime.getRuntime().halt(exitStatus)
方法 - JVM 宕机(搞笑脸)
- 如果 JVM 在 try 或 catch 块中达到了无限循环(或其他不间断,不终止的语句)
- 操作系统是否强行终止了 JVM 进程;例如,在 UNIX 上执行 kill -9 pid
- 如果主机系统死机;例如电源故障,硬件错误,操作系统死机等不会执行
- 如果 finally 块由守护程序线程执行,那么所有非守护线程在 finally 调用之前退出。
finalize 真的没用吗
我们上面简单介绍了一下 finalize 方法,并说明了它是一种不好的实践。那么 finalize 调用的时机是什么?为什么说 finalize 没用呢?
我们知道,Java 与 C++ 一个显著的区别在于 Java 能够自动管理内存
,在 Java 中,由于 GC 的自动回收机制,因而并不能保证 finalize
方法会被及时地执行(垃圾对象的回收时机具有不确定性),也不能保证它们会被执行。
也就是说,finalize 的执行时期不确定,我们并不能依赖于 finalize 方法帮我们进行垃圾回收,可能出现的情况是在我们耗尽资源之前,gc 却仍未触发,所以推荐使用资源用完即显示释放的方式,比如 close 方法。除此之外,finalize 方法也会生吞异常。
finalize 的工作方式是这样的:一旦垃圾回收器准备好释放对象占用的存储空间,将会首先调用 finalize
方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。垃圾回收只与内存有关。
我们在日常开发中并不提倡使用 finalize 方法,能用 finalize 方法的地方,使用 try...finally 会处理的更好。