陈同学
微服务
Accelerator
About
# 令人惊讶的Java异常真相-底层到底发生了什么? > 原文:[The Surprising Truth of Java Exceptions: What Is REALLY Going on Under the Hood?](https://blog.takipi.com/the-surprising-truth-of-java-exceptions-what-is-really-going-on-under-the-hood/) by Alex Zhitnitsky > > 翻译:[陈同学](https://chenyongjun.vip/) **Java异常和JVM底层的秘密:增长你的Java知识** 不像学习做一个香肠三明治,深入理解Java异常你肯定不会遗憾。 在本文中,我们将稍深入到JVM,了解异常抛出时JVM底层会发生什么、JVM如何存储异常信息并进行处理。 若你对异常的内部处理机制感兴趣,而不是浮于表面,那么这篇文章很适合你。 ## 了解一些基础异常流程和行为 异常是一个非常基础的Java概念,然而许多开发人员并未真正理解它。 在 [Joshua Bloch ](https://twitter.com/joshbloch)的《Effective Java》中,他给出了8个异常处理指导意见,下面是引用: * 仅在异常场景中使用异常 * 如果可以恢复,使用checked exception;如果是程序错误,使用runtime exception * 避免不必要的使用checked exception * 尽量使用标准异常 * 抛出合适的抽象异常 * 在方法注释中使用`@throws`声明要抛出的异常 * 在错误信息中包含捕获的故障信息 * 不要忽略异常 这种异常场景意味着在堆栈中需要有额外的上下文信息才能正确的处理异常,或者通过中断方法执行的方式避免导致对业务流程的破坏。我们收集一些参考资料以及对异常的理解,你可以 [点击查看](https://blog.takipi.com/java-exceptions-cheat-sheet/)。 > 译者注:为了便于大家查看,我将上述资料中的图片贴了出来 ![exceptions](https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2018/05/12/97f720220da440e093ed3b3d3cca0ce3.png) 如此说来,许多项目将异常作为其控制流程中的一部分来使用。一个常见的滥用情况是把异常当作 `GOTO` 语句一样使用,理想情况下,正常的异常根本就不应该存在。 我们这儿有一个经常性的调查课题,为了深入了解异常的行为,我们从60万个Java项目和1000个线上应用所产生的愈千万事件中收集了许多数据,你可以从 [这本电子书 ](http://land.overops.com/java-application-errors/)中了解更多信息。 ## JVM内部原理 我们可以通过一个非常简单的方式了解异常底层处理流程。为了确保顺利阅读,让我们先从几个关键的JVM概念开始。 ### 栈 - 线程私有 线程栈持有栈帧,栈帧中实时指向应用的当前执行位置。当方法通过正常返回或因抛出异常而终止时,栈帧的生命周期也就结束。 ### 堆内存 - 线程共享 堆内存用于管理应用运行时所需要的内存。关于内存管理和垃圾回收,我们也发布了一些文章:[GC overhead](https://blog.takipi.com/5-tips-for-reducing-your-java-garbage-collection-overhead/), [GC misconceptions](https://blog.takipi.com/7-things-you-thought-you-knew-about-garbage-collection-and-are-totally-wrong/), [garbage collectors comparisons](https://blog.takipi.com/garbage-collectors-serial-vs-parallel-vs-cms-vs-the-g1-and-whats-new-in-java-8/), [performance tuning](https://blog.takipi.com/java-performance-tuning-how-to-get-the-most-out-of-your-garbage-collector/). ### 非堆内存 - 线程共享 非堆内存指所有堆外分配的内存,它包含了我们待会将会详细介绍的异常表。 Java8中,永久代(PermGen) 被元空间(Metaspace)所取代,它们两用途一致,只是实现方式不同。二者最为显著的区别是:Java8开始,不需要再像指定PermGen size一样去指定Metaspace的大小,因为Metaspace会在运行时自动resize. ![JVM Internal](https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2018/05/12/70c4eb5b64074fd48729be90dc21760c.png) > 图片来源:[JVM Internals](http://blog.jamesdbloom.com/JVMInternals.html) by James Blooms ##非堆 异常表 异常表存储在每个方法的PermGen或Metaspace非堆空间中,如果方法中定义了`try-catch block`或`finally block`,将会创建异常表。 异常表有四个字段: * From : 开始点 * To:结束点 * Target:异常处理代码 * Type:异常类 当抛出异常时,JVM会使用异常表来定位异常处理者。如果不存在异常处理逻辑,栈帧生命周期结束,同时异常将根据stack trace 被继续抛给上层调用方法。 无论发生了什么异常,或者没有任何异常发生,最终的处理程序一定的执行。 ## 一个简单的异常处理流程 实践出真知,我们将创建一个简单的方法来演示异常底层处理机制。 ```java public static void main(String[] args) throws Exception { try { throw new Exception(); } catch (Exception e) { System.out.print("Caught!"); } finally { System.out.print("Finally!"); } } ``` 上面仅是简单的异常抛出、捕获、处理,那么我们怎么看到字节码和异常表呢? 通过下面命令查看.class文件的字节码: ```java javap -v sample.class ``` 下面是我们拿到的字节码(后面我们会提到一个有趣的"副作用"): ``` 0: new #17 // class java/lang/Exception 3: dup 4: invokespecial #19 // Method java/lang/Exception."<init>":()V 7: athrow 8: astore_1 9: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream; 12: ldc #26 // String Caught! 14: invokevirtual #28 // Method java/io/PrintStream.print:(Ljava/lang/String;)V 17: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream; 20: ldc #34 // String Finally! 22: invokevirtual #28 // Method java/io/PrintStream.print:(Ljava/lang/String;)V 25: goto 39 28: astore_2 29: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream; 32: ldc #34 // String Finally! 34: invokevirtual #28 // Method java/io/PrintStream.print:(Ljava/lang/String;)V 37: aload_2 38: athrow 39: return ``` 下面是异常表: ```java from to target type 0 8 8 Class java/lang/Exception 0 17 28 any ``` 好了,字节码听上去让人胆怯,你甚至可能对它非常陌生,但是不影响对于通用流程的理解。下面我们解析下异常表中发生了什么: * **异常表第一行**:它是我们的`try-catch`语句,如果在字节码的0-8行发生异常,将跳转到第8行进行处理 * **异常表第二行**:它是我们的`finally`语句,无论0-17行发生什么,最终都将由第28行进行处理。 OK,现在你已经知道什么是异常表,异常表的存储位置以及异常表和字节码之间的联系。 你可能注意到了上述过程中一些奇怪的地方,`Finally`在字节码中出现了两次。这是由于`javac`为了处理墨菲定律。 字节码8-25行的第一个`finally block`和`try-catch`相关,第二个`finally block`存在的意义是为了在`catch block`中重新抛出异常或其他原因导致中断正常流程时程序能够正常执行,注意下38行的字节码指令。 **总结上面的流程:创建异常并在字节码第7行抛出,接着异常表说:如果发生异常,请跳转到第8行。然后,我们打印了"Caught!"和"Finally!",最后跳到第39行,第39行是方法返回的地方。** 在字节码的第28-38行,我们拥有rethrow保护(即catch中再发生异常的保护机制)。 顺便说一句,如果你对异常性能优化感兴趣,可以参阅 Aleksey Shipilev的 [The Exceptional Performance of Lil’ Exception](https://shipilev.net/blog/2014/exceptional-performance/). 千万不要在非异常场景中使用异常。 ## 未捕获异常发生的恐怖故事 发生异常时我们do nothing将会发生什么? 当达到trace中最后的调用方法时,栈帧将结束; 如果没有任何异常handler,当前线程将死亡;如果当前线程是最后的非守护线程,JVM将死亡。这就是为什么我们总是推荐一定要有一个最后的异常handler. 我们已经见过许多案例,用户总是信誓旦旦的说他们应用中没有任何未捕获的异常,只是可能会有一些他们自己也不知道的甚至会导致严重破坏的错误。 ## 生产环境中管理异常最好方式是什么? 在Overops工作中,我们有机会致力于解决这个问题。无论生产环境中发生了什么异常或者输出了ERROR/WARN级别日志,我们捕获发生地的源代码以及整个调用过程的变量状态。 这意味着你可以看到导致每个异常发生的变量状态,而且可以轻松的重现异常。 如果没有这些信息,那么当你发现异常时你就只能在生产环境中痛苦的DEBUG,通过在日志中寻找线索,最终发现缺失必要的上下文信息,而且根本没有打印日志,最后只能加上Debug信息重新发布应用,期望异常能够再次重现。一次次的守候,直到你找到异常发生的真正原因。 ## 结语 衷心希望你能开心的学习更多的异常底层知识,以及异常处理和JVM底层机制、异常表之间的联系。如果你喜欢这篇文章或者有其他任何问题,可以通过以下方式联系我们。
本文由
cyj
创作,可自由转载、引用,但需署名作者且注明文章出处。
文章标题:
令人惊讶的Java异常真相-底层到底发生了什么?
文章链接:
https://chenyongjun.vip/articles/29
扫码或搜索 cyjrun 关注微信公众号, 结伴学习, 一起努力