今天咱们来讨论一下,程序中的错误处理。java
在任何一个稳定的程序中,都会有大量的代码在处理错误,有一些业务错误,咱们能够经过主动检查判断来规避,可对于一些不能主动判断的错误,例如 RuntimeException,咱们就须要使用 try-catch-finally
语句了。小程序
有人说,错误处理并不难啊,try-catch-finally
一把梭,try 放功能代码,在 catch 中捕获异常、处理异常,finally 中写那些不管是否发生异常,都要执行的代码,这很简单啊。app
处理错误的代码,确实并不难写,但是想把错误处理写好,也并非一件容易的事情。异步
接下来咱们就从实现到 JVM 原理,讲清楚 Java 的异常处理。jvm
学东西,我仍是推荐要带着问题去探索,提早思考几个问题吧:函数
既然是异常处理,确定是区分异常发生和捕获、处理异常,这也正是组成异常处理的两大要素。布局
在 Java 中,抛出的异常能够分为显示异常和隐式异常,这种区分主要来自抛出异常的主体是什么,显示和隐式也是站在应用程序的视角来区分的。性能
显示异常的主体是当前咱们的应用程序,它指的是在应用程序中使用 “throw” 关键字,主动将异常实例抛出。而隐式异常就不受咱们控制, 它触发的主体是 Java 虚拟机,指的是 Java 虚拟机在执行过程当中,遇到了没法继续执行的异常状态,续而将异常抛出。学习
对于隐式异常,在触发时,须要显示捕获(try-catch),或者在方法头上,用 "throw" 关键字声明,交由调用者捕获处理。 编码
在咱们编写异常处理代码的时候,主要就是使用前面介绍到的 try-catch-finally
这三种代码块。
catch 容许存在多个,用于针对不一样的异常作不一样的处理。若是使用 catch 捕获多种异常,各个 catch 块是互斥的,和 switch 语句相似,优先级是从上到下,只能选择其一去处理异常。
既然 try-catch-finally 存在多种状况,而且在发生异常和不发生异常时,表现是不一致的,咱们就分清楚来单独分析。
1. try块中,未发生异常
不触发异常,固然是咱们乐于看见的。在这种状况下,若是有 finally 块,它会在 try 块以后运行,catch 块永远也不会被运行。
2. try块中,发生异常
在发生异常时,会首先检查异常类型,是否存在于咱们的 catch 块中指定的待捕获异常。若是存在,则这个异常被捕获,对应的 catch 块代码则开始运行,finally 块代码紧随其后。
例如:咱们只监听了空指针(NullPointerException),此时若是发生了除数为 0 的崩溃(ArithmeticException),则是不会被处理的。
当触发了咱们未捕获的异常时,finally 代码依然会被执行,在执行完毕后,继续将异常“抛出去”。
3. catch 或者 finally 发生异常
catch 代码块和 finally 代码块,也是咱们编写的,理论上也是有出错的可能。
那么这两段代码发生异常,会出现什么状况呢?
当在 catch 代码块中发生异常时,此时的表现取决于 finally 代码块中是否存在 return 语句。若是存在,则 finally 代码块的代码执行完毕直接返回,不然会在 finally 代码块执行完毕后,将 catch 代码中新产生的异常,向外抛出去。
而在极端状况下,finally 代码块发生了异常,则此时会中断 finally 代码块的执行,直接将异常向外抛出。
再回头看看第一个问题,假如咱们写了一个方法,其中的代码被 try-catch-finally
包裹住进行异常处理,此时若是咱们在多个地方都有 return 语句,最终谁的会被执行?
如上图所示,在完整的 try-catch-finally
语句中,finally 都是最后执行的,假设 finally 代码块中存在 return 语句,则直接返回,它是优先级最高的。
通常咱们不建议在 finally 代码块中添加 return 语句,由于这会破坏并阻止异常的抛出,致使不宜排查的崩溃。
在 Java 中,全部的异常,其实都是一个个异常类,它们都是 Throwable 类或其子类的实例。
Throwable 有两大子类,Exception 和 Error。
一般,咱们只须要捕获 Exception 就能够了。但 Exception 中,有一个特殊的子类 RuntimeException,即运行时错误,它是在程序运行时,动态出现的一些异常。比较常见的就是 NullPointerException、ArrayIndexOutOfBoundsException 等。
Error 和 RuntimeException 都属于非检查异常(Unchecked Exception),与之相对的就是普通 Exception 这种属于检查异常(Checked Exception)。
全部检查异常都须要在程序中,用代码显式捕获,或者在方法中用 throw 关键字显式标注。其实意思很明显,要不你本身处理了,要不你抛出去让别人处理。
这种检查异常的机制,是在编译期间进行检查的,因此若是不按此规范处理,在编译器编译代码时,就会抛出异常。
对于异常处理的性能问题,实际上是一个颇有争议的问题,有人以为异常处理是多作了一些工做,确定对性能是有影响的。可是也有人以为异常处理的影响,和增长一个 if-else
属于同种量级,对性能的影响其实微乎其微,是在能够接受的范围内的。
既然有争议,最简单的办法是写个 Demo 验证一下。固然,咱们这里是须要区分不一样的状况,而后根据解决对比的。
一个最简单的 for 循环 100w 次,在其中作一个 a++
的自增操做。
try-catch
语句。a++
包在 try
代码块中。try
代码块中,触发一个异常。就是一个简单的 for 循环,就不贴代码了,异常经过 5/0
这样的运算,触发除数为 0 的 ArithmeticException 异常,并在 JDK 1.8 的环境下运行。
为了不影响采样结果,每一个例子都单独运行 10 遍以后,取平均值(单位纳秒)。
到这里基本上就能够得出结论了,在没有发生异常的状况下,try-catch 对性能的影响微乎其微。可是一旦发生异常,性能上则是灾难性的。
所以,咱们应该尽量的避免经过异常来处理正常的逻辑检查,这样能够确保不会由于发生异常而致使性能问题。
至于为何发生异常时,性能差异会有如此之大,就须要从 Java 虚拟机 JVM 的角度来分析了,后面会详细分析。
try-catch-finally
确实很好用,可是它并不能捕获,异步回调中的异常。try 语句里的方法,若是容许在另一个线程中,其中抛出的异常,是没法在调用者这个线程中捕获的。
这一点在使用的过程当中,须要特别注意。
接下来咱们从 JVM 的角度,分析 JVM 如何处理异常。
当异常发生时,异常实例的构建,是很是消耗性能的。这是因为在构造异常实例时,Java 虚拟机须要生成该异常的异常栈(stack trace)。
异常栈会逐一访问当前线程的 Java 栈帧,以及各类调试信息。包括栈帧所指向的方法名,方法所在的类名、文件名以及在代码中是第几行触发的异常。
这些异常输出到 Log 中,就是咱们熟悉的崩溃日志(崩溃栈)。
当把 Java 代码编译成字节码后,每一个方法都会附带一个异常表,其中记录了当前方法的异常处理。
下面直接举个例子,写一个最简单的 try-catch
类。
使用 javap -c
进行反编译成字节码。
能够看到,末尾的 Exceptions Table 就是异常表。异常表中的每一条记录,都表明了一个异常处理器。
异常处理器中,标记了当前异常监控的起始、结束代码索引,和异常处理器的索引。其中 from 指针和 to 指针标识了该异常处理器所监控的代码范围,target 指针则指向异常处理器的起始位置,type 则为最后监听的异常。
例如上面的例子中,main 函数中存在异常表,Exception 的异常监听代码范围分别是 [0,8)(不包括 8),异常处理器的索引为 11。
继续分析异常处理流程,还须要区分是否命中异常。
1. 命中异常
当程序发生异常时,Java 虚拟机会从上到下遍历异常表中全部的记录。当发现触发异常的字节码的索引值,在某个异常表中某个异常监控的范围内。Java 虚拟机会判断所抛出的异常和该条异常监听的异常类型,是否匹配。若是能匹配上,Java 虚拟机会将控制流转向至该此异常处理器的 target 索引指向的字节码,这是命中异常的状况。
2. 未命中异常
而若是遍历完异常表中全部的异常处理器以后,仍未匹配到异常处理器,那么它会弹出当前方法对应的 Java 栈帧。回到它的调用者,在其中重复此过程。
最坏的状况下,Java 虚拟机须要遍历当前线程 Java 栈上全部方法的异常表。
咱们写的代码,其实终归是给人读的,可是编译器干的事儿,都不是人事儿。它会把代码作一些特殊的处理,只是为了让本身更好解析和执行。
编译器对 finally 代码块,就是这样处理的。在当前版本的 Java 编译器中,会将 finally 代码块的内容,复制几份,分别放在全部可能执行的代码路径的出口中。
写个 Demo 验证一下,代码以下。
继续 javap -c
反编译成字节码。
这个例子中,为了更清晰的看到 finally 代码块,我在其中输出的一段 Log “run finally”。能够看到,编译结果中,包含了三份 finally 代码块。
其中,前两份分别位于 try 代码块和 catch 代码块的正常执行路径出口。最后一份则做为全局的异常处理器,监控 try 代码块以及 catch 代码块。它将捕获 try 代码块触发而且未命中 catch 代码块捕获的异常,以及在 catch 代码块触发的异常。
而 finally 的代码,若是出现异常,就不是当前方法所能处理的了,会直接向外抛出。
从上图中能够看到,在异常表中,还存在两个 any 的信息。
第一个信息的 from 和 to 的范围就是 try 代码块,等因而对 catch 遗漏异常的一种补充,表示会处理全部种类的异常。
第二个信息的 from 和 to 的范围,仔细看能看到它实际上是 catch 代码块,这也正好印证了咱们上面的结论,catch 代码块其实也被异常处理器监控着。
只是若是命中了 any 以后,由于没有对应的异常处理器,会继续向上抛出去,交由该方法的调用方法处理。
到这里咱们就基本上讲清楚了 Java 异常处理的全部内容。
在平常开发当中,应该尽可能避免使用异常处理的机制来处理业务逻辑,例如不少代码中,类型转换就使用 try-catch
来处理,实际上是很不可取的。
异常捕获对应用程序的性能确实有影响,但也是分状况的。
一旦异常被抛出来,方法也就跟着 return 了,捕获异常栈时会致使性能变得很慢,尤为是调用栈比较深的时候。
可是从另外一个角度来讲,异常抛出时,基本上代表程序的错误。应用程序在大多数状况下,应该是在没有异常状况的环境下运行的。因此,异常状况应该是少数状况,只要咱们不滥用异常处理,基本上不会影响正常处理的性能问题。
本文对你有帮助吗?留言、点赞、转发是最大的支持,谢谢!
「联机圆桌」👈推荐个人知识星球,一年 50 个优质问题,上桌联机学习。
公众号后台回复成长『成长』,将会获得我准备的学习资料,也能回复『加群』,一块儿学习进步;你还能回复『提问』,向我发起提问。
推荐阅读:
“寒冬”正是学习时|关于字符编码,你须要知道的都在这里 | 分词,科普及解决方案| 图解:HTTP 范围请求 | 小程序学习资料 |HTTP 内容编码 | 辅助模式实战 | 辅助模式玩出花样 | 小程序 Flex 布局