【JVM虚拟机】(9)-- JVM是如何处理异常的

【JVM虚拟机】(9)-- JVM是如何处理异常的

上篇博客我们简单说过异常信息是存放在属性表集合中的Code属性表里,那么这篇博客就单独讲Code属性表中的exception_table。

在讲之前我们先思考两个问题?

1、为什么捕获异常会较大的性能消耗?

2、为什么finally中的代码会永远执行?

接下来会从JVM虚拟机的角度来解答这两个问题。

一、概念

1、JVM是如何捕获异常的?

1、编译而成的字节码中,每个方法都附带一个异常表
2、异常表中每一个条目代表一个异常处理器
3、触发异常时,JVM会遍历异常表,比较触发异常的字节码的索引值是否在异常处理器的from指针到to指针的范围内。
4、范围匹配后,会去比较异常类型和异常处理器中的type是否相同
5、类型匹配后,会跳转到target指针所指向的字节码(catch代码块的开始位置)
6、如果没有匹配到异常处理器,会弹出当前方法对应的Java栈帧,并对调用者重复上述操作。

2、什么是异常表?

1. 每个方法都附带一个异常表

2. 异常表中每一个条目, 就是一个异常处理器

异常表如下:

3、什么是异常处理器?其组成部分有哪些?

1、异常处理器由from指针、to指针、target指针,以及所捕获的异常类型所构成(type)。
2、这些指针的数值就是字节码的索引(bytecode index, bci),可以直接去定位字节码。
3、from指针和to指针,标识了该异常处理器所监控的返回
4、target指针,指向异常处理器的起始位置。如catch代码块的起始位置
5、type:捕获的异常类型,如Exception

4、如果在方法的异常表中没有匹配到异常处理器,会怎么样?

1、会弹出当前方法对应的Java栈帧
2、在调用者上重复异常匹配的流程。
3、最坏情况下,JVM需要编译当前线程Java栈上所有方法的异常表

二、代码演示

1、try-catch

public static void main(String[] args) {
  try {
    mayThrowException();
  } catch (Exception e) {
    e.printStackTrace();
  }
}
// 对应的 Java 字节码
public static void main(java.lang.String[]);
  Code:
    0: invokestatic mayThrowException:()V
    3: goto 11
    6: astore_1
    7: aload_1
    8: invokevirtual java.lang.Exception.printStackTrace
   11: return
  Exception table:
    from  to target type
      0   3   6  Class java/lang/Exception  // 异常表条目

上面Code中的字节码自己也没有仔细研究过,我们可以具体看下面的Exception table表,来进行分析。

1、from和to: 指是try和catch之间的代码的索引位置。from=0,to=3,代表从字节索引0的位置到3(不包括3)。

2、target : 代表catch后代码运行的起始位置。

3、type : 指的是异常类型,这里是指Exception异常。

当程序触发异常时,java虚拟机会从上至下遍历异常表中的所有条目。当触发异常的字节码的索引值在某个异常表条目的监控范围内,Java 虚拟机会判断所抛出的异常
和该条目想要捕获的异常是否匹配。如果匹配,Java 虚拟机会将控制流转移至该条目target 指针指向的字节码。

如果遍历完所有异常表条目,Java 虚拟机仍未匹配到异常处理器,那么它会弹出当前方法对应的Java 栈帧,并且在调用者(caller)中重复上述操作。在最坏情况下,
Java 虚拟机需要遍历当前线程 Java 栈上所有方法的异常表。

2、try-catch-finally

finally 代码块的编译比较复杂。当前版本 Java 编译器的做法,是复制 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中

代码示例

public static void XiaoXiao() {
   try {
       dada();
   } catch (Exception e) {
       e.printStackTrace();
   } finally {
       System.out.println("Finally");
   }
}
//通过javap 反编译
public static void XiaoXiao();
    Code:
       0: invokestatic  #3                  // Method dada:()V
       3: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
       6: ldc           #7                  // String Finally
       8: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      11: goto          41
      14: astore_0
      15: aload_0
      16: invokevirtual #5                  // Method java/lang/Exception.printStackTrace:()V
      19: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
      22: ldc           #7                  // String Finally
      24: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      27: goto          41
      30: astore_1
      31: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
      34: ldc           #7                  // String Finally
      36: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      39: aload_1
      40: athrow
      41: return
    Exception table:
       from    to  target type
           0     3    14   Class java/lang/Exception
           0     3    30   any
          14    19    30   any

和之前有所不同,这次

1、异常表中,有三条数据,而我们仅仅捕获了一个Exception
2、异常表的后两个item的type为any

上面的三条异常表item的意思为

1、如果0到3之间,发生了Exception类型的异常,调用14位置的异常处理者。
2、 如果0到3之间,无论发生什么异常,都调用30位置的处理者。
3、 如果14到19之间(即catch部分),不论发生什么异常,都调用30位置的处理者。

问题:通过上面那幅图和javap反编译代码就可以很好的解释为什么finally里面的代码永远会执行?
原因:因为当前版本Java编译器的做法,是复制finally代码块的内容,分别放到所有正常执行路径,以及异常执行路径的出口中

这三份finally代码块都放在什么位置:
第一份位于try代码后 : 若果try中代码正常执行,没有异常那么finally代码就在这里执行。
第二份位于catch代码后 : 如果try中有异常同时被catch捕获,那么finally代码就在这里执行。
第三份位于异常执行路径 : 如果如果try中有异常但没有被catch捕获,或者catch又抛异常,那么就执行最终的finally代码。

问题 :为什么捕获异常会较大的性能消耗?
因为构造异常的实例比较耗性能。这从代码层面很难理解,不过站在JVM的角度来看就简单了,因为JVM在构造异常实例时需要生成该异常的栈轨迹。这个操作会逐一访问当前
线程的栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常等信息。虽然具体不清楚JVM的实现细节,但
是看描述这件事情也是比较费时费力的。

参考

深入拆解 Java 虚拟机(郑雨迪)

只要自己变优秀了,其他的事情才会跟着好起来(少将7)

原文地址:https://www.cnblogs.com/qdhxhz/p/10765839.html

时间: 2024-10-10 00:42:33

【JVM虚拟机】(9)-- JVM是如何处理异常的的相关文章

深入JVM虚拟机(二) JVM运行机制

深入JVM虚拟机(二) JVM运行机制 1 JVM运行机制 1.1 JVM启动流程 JVM是Java程序运行的环境,同时是一个操作系统的一个应用程序进程,因此它有自己的生命周期,也有自己的代码和数据空间.JVM工作原理和特点主要是指操作系统装入JVM,是通过jdk中Java.exe来完成通过下面4步来完成JVM环境. 1.创建JVM装载环境和配置. 2.装载JVM.dll. 3.初始化JVM.dll并挂界到JNIENV(JNI调用接口)实例. 4.调用JNIEnv实例装载并处理class类. J

[转帖]Java虚拟机(JVM)体系结构概述及各种性能参数优化总结

Java虚拟机(JVM)体系结构概述及各种性能参数优化总结 2014年09月11日 23:05:27 zhongwen7710 阅读数 1437 标签: JVM调优jvm 更多 个人分类: Java知识点总结技术架构原理 https://blog.csdn.net/zhongwen7710/article/details/39213377 写的很好.. 堆栈分不清楚的我 愧对计算机系毕业.. 第一部分:相关的概念 数据类型 Java虚拟机中,数据类型可以分为两类:基本类型和引用类型.基本类型的变

Java虚拟机(JVM)中的内存设置详解

在一些规模稍大的应用中,Java虚拟机(JVM)的内存设置尤为重要,想在项目中取得好的效率,GC(垃圾回收)的设置是第一步. PermGen space:全称是Permanent Generation space.就是说是永久保存的区域,用于存放Class和Meta信息,Class在被Load的时候被放入该区域Heap space:存放Instance. GC(Garbage Collection)应该不会对PermGen space进行清理,所以如果你的APP会LOAD很多CLASS的话,就很

Atitit .jvm 虚拟机指令详细解释

Atitit .jvm 虚拟机指令详细解释 1. 一.未归类系列A1 2. 数据mov系列2 2.1. 二.const系列2 2.2. 三.push系列2 2.3. ldc系列 该系列命令负责把数值常量或String常量值从常量池中推送至栈顶.3 2.4. 5.1.load系列A 该系列命令负责把本地变量的送到栈顶.3 2.5. 5.2.load系列B 该系列命令负责把数组的某项送到栈顶.4 2.6. 6.1.store系列A 该系列命令负责把栈顶的值存入本地变量.5 2.7. 6.2.stor

详细介绍Java虚拟机(JVM)

1. JVM生命周期 启动.启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点. 运行.main()作为该程序初始线程的起点,任何其他线程均由该线程启动. 消亡.当程序中的所有非守护线程都终止时,JVM才退出:若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出. 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序.程序开

JVM(虚拟机)原理

一.Java虚拟机的生命周期: Java虚拟机的生命周期 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序.程序开始执行时他才运行,程序结束时他就停止.你在同一台机器上运行三个程序,就会有三个运行中的Java虚拟机. Java虚拟机总是开始于一个main()方法,这个方法必须是公有.返回void.直接受一个字符串数组.在程序执行时,你必须给Java虚拟机指明这个包换main()方法的类名. Main()方法是程序的起点,他被执行的线程初始化为程序的初始线程.程序中其他的线程都由他来

初谈JVM虚拟机

学了这么久的Java,一直听说JVM虚拟机是运行所有java程序,但是不知道具体内部结构是怎样,以及它的运行机制是什么.今天刚好看到一篇文章,索性就开始学习. JVM的主要结构: 由上图可以看出,Jvm主要组成有:类加载器.运行数据区.执行引擎.本地方法接口组成.其中运行数据区包含子模块方法区.堆.Java栈.本地方法栈以及寄存器.对于方法区.堆是对所有线程共享的,而其他则是属于当前线程私有. 下面开始一步步剖析JVM........... 1.类加载器(Class loader) 类加载器负责

JVM虚拟机结构

JVM的主要结构如下图所示,图片引用自舒の随想日记. JVM结构 方法区和堆由所有线程共享,其他区域都是线程私有的 程序计数器(Program Counter Register) 类似于PC寄存器,是一块较小的内存区域,通过程序计数器中的值寻找要执行的指令的字节码,由于多线程间切换时要恢复每一个线程的当前执行位置,所以每个线程都有自己的程序计算器.这一个区域不会有OutOfMemeryError.当执行Java方法时,这里存储的执行的指令的地址,如果执行的是本地方法,这里的值是Undefined

JVM虚拟机(一) 内存区域

JVM虚拟机内存组成: 如下图: 1. 程序计数器: (1)是一块较小的内存空间:可以看做当前程序执行子界面的行号指示器,字节码解析器执行的时候就是根据这个判断下一条指令该执行什么. (2)因为cpu在执行代码的时候,会在多个线程之间进行切换执行,所以为了在cup切换后恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各线程之间互不影响,独立存在,我们称此类内存为"线程私有"的内存. (3) 如果当前cup执行的是java代码,这个计数器是记录的是正在执行的虚拟机字节码的地址:如