揭开JVM所看到的try/catch/finally

简介: #揭开JVM所看到的try/catch/finally 最近有一位朋友发了一段代码给我,这个方法很简单,具体内容大致如下: ```java int num = 5000000;//500万 long begin = System.currentTimeMillis(); for(int i=0; i

揭开JVM所看到的try/catch/finally

最近有一位朋友发了一段代码给我,这个方法很简单,具体内容大致如下:

int num = 5000000;//500万 
long begin = System.currentTimeMillis();
for(int i=0; i<num; i++){
   try{ 
        //do something
      }catch(Exception e){

      }
 }
long end = System.currentTimeMillis();
System.out.println("==============使用时间:" + (end - begin) + " 毫秒");

上面代码可以看到是通过执行该循环体所消耗的时间,通过和把try/cache注释掉进行对比,最后得到的结果时间比较随机,执行的耗时和try/cache没有必然的联系,那try/cache究竟会不会影响代码的执行效率呢?从java语言的源码上看貌似多执行了一些指令,实际上是怎么样的呢?下面我分几个场景来分析一下jvm对try/cache的处理过程。

单层的try/catch

下面是一个只有单层的try/catch代码块

 public int test(int a,int b){
        try{
            return a+b;
        }catch (Exception e){
            throw new CustomException();
        }
    }

通过javap -v查看JVM编译成class字节码之后是如何处理这个try/catch

public int test(int, int);
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=3
         0: iload_1                          // 将第一个int参数压入队列(第一个入参)
         1: iload_2                          // 将第二个int参数压入队列(第二个入参)
         2: iadd                             //弹出队列中第一个和第二个参数执行相加,并把相加结果压入队列
         3: ireturn                          //弹出队列第一个元素,并return。
         4: astore_3                         //此处是try开始的逻辑
         5: new           #3                 // class com/bieber/demo/CustomException
         8: dup                                
         9: invokespecial #4                 // Method com/bieber/demo/CustomException."<init>":()V
        12: athrow                           //将队列中的第一个元素弹出,并当做异常抛出,到此整个方法体完毕
     Exception table:
         from    to  target type
             0     3     4   Class java/lang/Exception
     LineNumberTable:
        line 13: 0
        line 14: 4
        line 15: 5
     LocalVariableTable:
        Start  Length  Slot  Name   Signature
               5       8     3     e   Ljava/lang/Exception;
               0      13     0  this   Lcom/cainiao/cilogisticservice/ExceptionClass;
               0      13     1     a   I
               0      13     2     b   I
     StackMapTable: number_of_entries = 1
          frame_type = 68 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]

上面是test方法JVM编译之后的结果,上面的Code块是整个方法体的内容,而从0-3可以视为是方法体的正常逻辑,4-12可以视为try/catch块,从方法体的指令看,正常情况下执行到3的地方就完毕了,而不会去执行4-12的指令。那是不是就得出结论,try/catch代码块在正常逻辑的时候是不会被执行的,于是对于对代码加上try/catch块,并不会影响代码的执行效率,因为根本不会有多余的指令被执行,只有出现异常的时候才会多出执行异常的指令。其实本文到这里基本上可以结束了,因为得到了我想要的答案(try/catch代码块对代码性能的影响)。为了让整个问题能够更加全面一点,下面对JVM如何处理一个try/catch做更加深入的调研。

上面的JVM编译的字节码的时候除了Code代码块,还有Exception table代码块,从这个代码块的内容可以看到,包含四列(from,to,target,type),其中fromto表示这个try/catch代码块是从哪开始到哪结束,可以看到上面的try/catch代码块是从Code代码块的0-3,也就是从加载第一个int值到返回结果的代码块,target表示这个try/catch代码块执行逻辑在哪里开始,比如上面的表示从Code中的4开始,也就是astore_3指令开始,直到athrow指令被执行的地方,在Exception table中的一行还有type列,表示是这个异常类型,用于在一个try/catch代码块出现多个catch内容,用于匹配正确的异常类型。下面我列出这种情况:

一个try对应多个catch

我将上面的代码调整成了下面结构:

 public int test(int a,int b){
        try{
            return a+b;
        }catch (Exception e){
            a++;
            throw new CustomException();
        }catch (Throwable t){
            b++;
            throw new CustomException();
        }
    }

JVM对上面代码编译后的结果:

 public int test(int, int);
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=3
         0: iload_1       
         1: iload_2       
         2: iadd          
         3: ireturn       
         4: astore_3      
         5: iinc          1, 1
         8: new           #3                  // class com/bieber/demo/CustomException
        11: dup           
        12: invokespecial #4                  // Method com/bieber/demo/CustomException."<init>":()V
        15: athrow        
        16: astore_3      
        17: iinc          2, 1
        20: new           #3                  // class com/cainiao/cilogisticservice/CustomException
        23: dup           
        24: invokespecial #4                  // Method com/cainiao/cilogisticservice/CustomException."<init>":()V
        27: athrow        
      Exception table:
         from    to  target type
             0     3     4   Class java/lang/Exception
             0     3    16   Class java/lang/Throwable
      LineNumberTable:
        line 13: 0
        line 14: 4
        line 15: 5
        line 16: 8
        line 17: 16
        line 18: 17
        line 19: 20
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               5      11     3     e   Ljava/lang/Exception;
              17      11     3     t   Ljava/lang/Throwable;
               0      28     0  this   Lcom/cainiao/cilogisticservice/ExceptionClass;
               0      28     1     a   I
               0      28     2     b   I
      StackMapTable: number_of_entries = 2
           frame_type = 68 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]
           frame_type = 75 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]

和上面的内容对比一下会发现,在Code中多出了一段astore_3/athrow块,并且在Exception table中多了一行,想想通过上面的解释,对这个多出的一行的目的应该都知道是用来什么的,由于我在catch中成了throw之外,还多了一个++的操作,可以看到在astore_3/athrow块中多出了iinc指令,所以可以理解,try/catch在JVM中对应的是一个子代码块,在条件满足(出现匹配的catch异常)的时候会被执行。

下面我整理一下当出现异常的(这里说的是有try/catch的异常)JVM处理流程:

1、在try/catch出现异常
2、JVM会去`Exception table`查找匹配的异常类型
3、假设匹配上了,那么读取from,to,target,获取待执行的`try/catch`块的指令(具体是否抛出,看是否有athrow指令)。

为了更加了解JVM对try的处理,下面对try/finally再调研一下。

try/finally块的执行处理

调整代码逻辑,如下:

public int test(int a,int b){
        try{
            return a+b;
        }catch (Exception e){
            a++;
            throw new CustomException();
        }finally {
            b++;
        }
    }

JVM编译后的指令:

  public int test(int, int);
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=5, args_size=3
         0: iload_1       
         1: iload_2       
         2: iadd          
         3: istore_3                         //将栈顶的元素存储局部变量数组的第三个位置
         4: iinc          2, 1               //执行b++
         7: iload_3                          //把局部变量第三个位置的数值压入栈顶
         8: ireturn                          //弹出栈顶,并且返回
         9: astore_3      
        10: iinc          1, 1               //a++
        13: new           #3                  // class com/bieber/demo/CustomException
        16: dup           
        17: invokespecial #4                  // Method com/bieber/demo/CustomException."<init>":()V
        20: athrow        
        21: astore        4
        23: iinc          2, 1                //b++
        26: aload         4
        28: athrow        
      Exception table:
         from    to  target type
             0     4     9   Class java/lang/Exception
             0     4    21   any
             9    23    21   any
      LineNumberTable:
        line 13: 0
        line 18: 4
        line 14: 9
        line 15: 10
        line 16: 13
        line 18: 21
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
              10      11     3     e   Ljava/lang/Exception;
               0      29     0  this   Lcom/cainiao/cilogisticservice/ExceptionClass;
               0      29     1     a   I
               0      29     2     b   I
      StackMapTable: number_of_entries = 2
           frame_type = 73 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]
           frame_type = 75 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]

通过上面的代码,你会发现在Exception table都出了两行,其实我们只是在代码中只有一个try/catch块,而这里出现了三个,那么另外两个是做什么的呢?可以看到多出的两行的type都是any,这里的any表示的是任何异常类型,多出的第一行,是从0-4,表示0-4之间的指令出现异常,会从21的指令开始执行,发现执行的是b++(finally)的内容,多出的第二行是9-23,表示9-23之间的指令被执行的过程中出现异常也会从21行开始执行(也是执行finally的内容),而9-23其实是catch的代码逻辑。上面均是出现了异常会触发finally的代码执行,正常情况下会发现4的位置执行了finally的内容,然后再执行ireturn指令,这里可以得出,JVM处理finally其实是对于正常的指令队列增加了finally代码块的指令,以及对异常中添加了finally代码块的指令,这也就导致了fianlly在任何地方都可以被执行,其实就是冗余了指令队列(其实思想比较简单)。

到此,对JVM如何处理try/catch/finally块进行了简单的介绍,目的是让大家对添加try代码块不要吝啬,在需要的时候,还是需要做一些异常的控制,让代码的异常逻辑更加完善,而不是一直将异常抛给外面处理,因为外面可能并不知道你这个异常是什么意思。

PS:如有任何理解错误的地方,欢迎指出,大家共同学习。

相关文章
|
6月前
|
Java Devops
【JVM】Try to Avoid -XX UseGCLogFileRotation
【JVM】Try to Avoid -XX UseGCLogFileRotation
84 1
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4
|
3月前
|
Java Docker 索引
记录一次索引未建立、继而引发一系列的问题、包含索引创建失败、虚拟机中JVM虚拟机内存满的情况
这篇文章记录了作者在分布式微服务项目中遇到的一系列问题,起因是商品服务检索接口测试失败,原因是Elasticsearch索引未找到。文章详细描述了解决过程中遇到的几个关键问题:分词器的安装、Elasticsearch内存溢出的处理,以及最终成功创建`gulimall_product`索引的步骤。作者还分享了使用Postman测试接口的经历,并强调了问题解决过程中遇到的挑战和所花费的时间。
|
11天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
9天前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
8 1
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
65 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
存储 缓存 算法
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
|
28天前
|
存储 算法 Java
聊聊jvm的内存结构, 以及各种结构的作用
【10月更文挑战第27天】JVM(Java虚拟机)的内存结构主要包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和运行时常量池。各部分协同工作,为Java程序提供高效稳定的内存管理和运行环境,确保程序的正常执行、数据存储和资源利用。
46 10
|
27天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
1月前
|
存储 监控 算法
JVM调优深度剖析:内存模型、垃圾收集、工具与实战
【10月更文挑战第9天】在Java开发领域,Java虚拟机(JVM)的性能调优是构建高性能、高并发系统不可或缺的一部分。作为一名资深架构师,深入理解JVM的内存模型、垃圾收集机制、调优工具及其实现原理,对于提升系统的整体性能和稳定性至关重要。本文将深入探讨这些内容,并提供针对单机几十万并发系统的JVM调优策略和Java代码示例。
51 2