为何不建议在 for 循环里捕捉异常?

在回答标题这个问题以前,咱们先试想一下,在没有 try…catch 的状况下,若是想要对函数的异常结果进行判断,咱们应该怎么作?html

异常

第一个想法确定就是 if…else 了,通常状况下,相关的代码段咱们都是放在一块儿的,若是此时你的程序中有大量的代码段要作这作判断,这就意味着后面执行的逻辑会依赖你前面语句的执行状况,也就意味着你每调用一个可能会出现错误的函数的时候,都要先判断是否成功,而后再继续执行后面的语句。这就会致使你的代码中会充斥着大量的 if…else。java

Java 是一门工程性的语言,而工程也是一种艺术,所以采用这样的作法显然是很不优雅的。《Thinking in Java》中提到“badly formed code will not be run.”,意思是结构不优雅的代码不该该被执行,因而一个适用于 Java 的异常处理机制便应运而生了。程序员

Java 的异常处理其目的在于经过使用少于目前数量的代码来简化大型程序,举个简单的例子 🌰web

不用 try…catch微信

FileReader fr = new FileReader("path");if (fr == null) { System.err.println("Open File Error");} else { BufferedReader br = new BufferedReader(fr); while (br.ready()) { String line = br.readLine(); if (line == null) { System.err.println("Read Line Error"); } else { System.out.println(line); } }}

用了 try…catchoracle

try { FileReader fr = new FileReader("path"); BufferedReader br = new BufferedReader(fr); while (br.ready()) { String line = br.readLine(); System.out.println(line); }} catch (IOException e) { e.printStackTrace();}

很明显咱们能够看出来,下面这种写法主线明确,可读性更高。框架

固然,try…catch 也并非百利而无一害。若是程序员在代码中滥用了 try…catch,而且没有作好异常处理,颇有可能会致使一些 bug 被隐藏,没法跟踪。不过这些不是本文的重点。有兴趣的能够去阅读下《Thinking in Java》的第 12 章「经过异常处理错误」。jvm

单独捕获异常

在探究将异常捕获与循环结合起来以前,咱们先看一下单独捕获一个异常会发生什么?这是一段异常代码函数

咱们用 javap -c ExceptionDemo.class 来打印出他的字节码来看一下性能

指令含义不是本文的重点,因此这里就不介绍具体的含义,感兴趣能够到 Oracle 官网查看相应指令的含义 👉The Java Virtual Machine Instruction Set[1]

异常表的四个参数

从输出看,字节码分两部分,code(指令)和 exception table(异常表)两部分。当将 java 源码编译成相应的字节码的时候,若是方法内有 try catch 异常处理,就会产生与该方法相关联的异常表,也就是Exception table:部分。

每个条目有四列信息: 异常声明的开始行, 结束行, 异常捕获后跳转到的代码计数器(PC)所指向的行数, 还有一个表示捕获的异常类的常量池索引。

那这些信息是从哪来得到的呢?这里咱们先来来复习一下 JVM 的相关知识:

一个线程就是一个栈,由栈帧组成,一个方法就是一个栈帧,内部保存着:局部变量表、操做数栈、动态连接、方法出口。

JVM 在构造异常实例时须要生成该异常的栈轨迹。这个操做会逐一访问当前线程的栈帧,而且记录下各类调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常等信息。而这些信息就会存储在刚才所说的Exception table:中。

四个参数的做用

那刚才所说的那些信息又有什么用呢?

若是在执行方法时有一个异常被抛出, JVM 就会从异常表中按照条目所出现的顺序查找对应的条目。若是异常抛出时 PC 计数器所指向的行数正好落在异常表中某一条目包含的范围内, 而且所抛出的异常正好是异常表中 type 列所指定的异常(或者所指定异常的子类), 那么 JVM 就会将 PC 计数器指向 Target 偏移量所指向的地址, (进入 catch 块)继续执行。

若是没有在异常表中找到异常, JVM 就会将当前栈帧弹出并从新抛出这个异常。当 JVM 弹出当前栈帧的时候, 它就会停止当前方法的执行, 返回到调用当前方法的外部方法中, 不过并不会像正常没有异常发生时那样继续执行外部方法, 而是在外部方法中抛出相同的异常, 这样将会致使 JVM 会在外部方法中重复查询异常表并处理异常的过程。

为何捕获异常消耗性能

其实从上面的分析中,咱们就已经能够理解为何捕获异常是一个消耗性能的操做了,当你 new 一个 exception 的时候,JVM 已经在 exception 里构建好了全部的 stacktrace:

如今 Java 领域最火的框架莫过于 Spring 系列了,在一个 web 项目中,调用栈的深度是至关大的,因而可知这里花费的代价是可观的,所以,当你对 stacktrace 不感兴趣的时候,不须要这样的信息时,最好不要随便的 new exception。

异常+for 循环

说了那么多其实都是前置知识,如今咱们终于来到了标题提到的问题了。

for 循环和异常有两种结合方式:

 try+for 循环

public static void tryFor() { int j = 3; try { for (int i = 0; i < 1000; i++) { Math.sin(j); } } catch (Exception e) { e.printStackTrace(); }}

for 循环+try

public static void forTry() { int j = 3; for (int i = 0; i < 1000; i++) { try { Math.sin(j); } catch (Exception e) { e.printStackTrace(); } }}

首先我先给出结论: 在没有发生异常时,二者性能上没有差别。若是发生异常,二者的处理逻辑不同,虽然已经不具备比较的意义了,但 for 循环+try 的耗时更明显。

字节码比较

咱们对这两种方式进行一个字节码的比较:

经过第二节的分析咱们知道,当程序出现异常时,java 虚拟机就会查找方法对应的异常表,若是发现有声明的异常与抛出的异常类型匹配就会跳转到 catch 处执行相应的逻辑,若是没有匹配成功,就会回到上层调用方法中继续查找,如此反复,一直到异常被处理为止,或者中止进程。而在 for 循环中进行 try…catch 操做,会不断的进行这一过程,性能损耗天然会很恐怖。

测试比较

说了这么多咱们一直都是纸上谈兵,口说无凭,实际的效果确定是要跑一下才知道,这里咱们采用 Java 的一个微基准测试框架JMH[2]来进行这次测试。

测试结果

Benchmark Mode Cnt Score Error UnitsExceptionDemo.forTry thrpt 20 70.236 ± 8.945 ops/msExceptionDemo.tryFor thrpt 20 85.864 ± 3.272 ops/ms

score 的结果是 xxx ± xxx,单位是每毫秒多少个操做。最终结果也验证了咱们的结论。tryFor 的确会比 forTry 更节省性能。

最后

本文从异常出发,分析了单独捕获异常和将异常与 for 循环结合的几种不一样的状况,而后经过 JMH 进行了一次测试,最终验证咱们标题所说的,不建议在 for 循环里捕捉异常。

固然,try…catch 对性能的影响除了第二节所提到的须要维护一个异常表以外,还有一个缘由,那就是 try 块会阻止 java 的优化(例如重排序),try catch 里面的代码是不会被编译器优化重排的。固然重排序是须要必定的条件触发。通常而言,只要 try 块范围越小,对 java 的优化机制的影响是就越小。因此保证 try 块范围尽可能只覆盖抛出异常的地方,就可使得异常对 java 优化的机制的影响最小化。

以上就是本文的所有内容了,若是你以为有所帮助,不妨点个赞支持一下。


References

[1] The Java Virtual Machine Instruction Set: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html
[2] JMH: http://openjdk.java.net/projects/code-tools/jmh/


本文分享自微信公众号 - 01二进制(gh_d1999add1857)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索