java 异常-性能及实现机制分析

网络上好多文章都分析了java语言try-catch-finally语句(下文中称try语句)的使用及返回值问题,但是对try语句的性能及其实现机制较少提及,本文依据try语句生成的字节码文件,分析try语句的性能及实现机制。

try语句的性能

问题由来

工作期间,曾被人问到过下面这样的一个问题,请看下面测试代码

public class Test {
    public static void main ( String[] args ) {
        outerTry( args );
        innerTry( args );       
    }

    private static void outerTry ( String[] args ) {
        try {
            for ( String arg : args ) {
                tryMethod();
            }
        }catch ( Exception e ){
            catchMethod();
        }

    }

    private static void innerTry ( String[] args ) {
        for ( String arg : args ) {
            try{
                tryMethod();
            }catch ( Exception e ){
                catchMethod();
            }
        }
    }

    private static void tryMethod () { }

    private static void catchMethod () { }
}

上述代码中,如果在tryMethod()方法中没有抛出异常,那么innerTry ( String[] args )outerTry ( String[] args )在性能上有多大差别?

分析字节码文件

对代码进行反编译,通过字节码文件分析差异,执行下面语句

javap -c -p Test.class

下面仅保留了innerTry()outerTry()方法的字节码

  private static void outerTry(java.lang.String[]);
    Code:
       0: aload_0
       1: astore_1
       2: aload_1
       3: arraylength
       4: istore_2
       5: iconst_0
       6: istore_3
       7: iload_3
       8: iload_2
       9: if_icmpge     26
      12: aload_1
      13: iload_3
      14: aaload
      15: astore        4
      17: invokestatic  #4                  // Method tryMethod:()V
      20: iinc          3, 1
      23: goto          7
      26: goto          33
      29: astore_1
      30: invokestatic  #6                  // Method catchMethod:()V
      33: return
    Exception table:
       from    to  target type
           0    26    29   Class java/lang/Exception


 private static void innerTry(java.lang.String[]);
    Code:
       0: aload_0
       1: astore_1
       2: aload_1
       3: arraylength
       4: istore_2
       5: iconst_0
       6: istore_3
       7: iload_3
       8: iload_2
       9: if_icmpge     34
      12: aload_1
      13: iload_3
      14: aaload
      15: astore        4
      17: invokestatic  #4                  // Method tryMethod:()V
      20: goto          28
      23: astore        5
      25: invokestatic  #6                  // Method catchMethod:()V
      28: iinc          3, 1
      31: goto          7
      34: return
    Exception table:
       from    to  target type
          17    20    23   Class java/lang/Exception

为下面叙述方便,将code:下面的0,1 等标号,称为第0行,第1行。实际上表示的是JVM指令所在的内存位置,不同的指令,指令长度可能不一致,因此可以发现值是不连续的。
先看两个方法的指令码,发现第0-17行都是一致的,后面开始有差异。
innerTry的第17行执行后,执行了goto语句跳转到第28行指令(第20行语句),第28行是iinc指令,与outerTry中的20行一致,interTry中第28行结束后,第31行执行了goto语句,与outerTry中的23行是一致的,全部跳转到了第7行。因此两者的差异仅在于innerTryouterTry多执行了一条1个字节长度的goto指令,因此两者的性能可以认为是没有差异的。

小结

通过上面分析可以看到,两者差别仅在于一条goto语句,所以可以认为两者的性能是没有差异的

try-catch语句的实现机理

新的问题来了,我们的代码及反编译代码中都有catch语句,那么catch语句是如何被找到的呢?
java对try语句的执行是通过异常表进行的
innerTryouterTry反编译下面都有如下所示的反编译代码

    Exception table:
       from    to  target type
          17    20    23   Class java/lang/Exception

这个就是异常表,上面的异常表来自于innerTry方法。
异常表有四部分组成,fromtotargettypefrom表示从哪个字节指令开始,to表示到哪个字节指令结束(不包含),target表示如果发生异常了,下一步应该执行哪个指令,type表示匹配的异常,如果这个异常没有匹配上,是不会执行target地方的指令的。因此,上面的异常表表明,从第17行指令开始,到第20行指令(不包含)结束(正好是tryMethod),如果发生Exception类型的异常,就去执行第23行的代码,而第23行的代码是astore(将异常对象压入栈),第25行代码是执行catchMethod方法。
代码进行编译时,编译器根据try语句生成异常表。当发生异常时,查询异常表是根据顺序进行的,因此,大家就理解,为什么如果有多个异常的时候,需要把先想要匹配的放在前面。如果在这个异常表中指不到相应的处理代码。

finally语句块的实现机制及其对返回值和异常的影响

我们来看下面的代码

public static void main ( String[] args ) {
        try {
            tryMethod();
        }catch ( Exception e ){
            catchMethod();
        }finally {
            finallyMethod();
        }
    }

    private static void finallyMethod () { }

    private static void tryMethod () { }

    private static void catchMethod () { }

通过反编译,查看字节码文件

public static void main(java.lang.String[]);
    Code:
       0: invokestatic  #2                  // Method tryMethod:()V
       3: invokestatic  #3                  // Method finallyMethod:()V
       6: goto          25
       9: astore_1 
      10: invokestatic  #5                  // Method catchMethod:()V 
      13: invokestatic  #3                  // Method finallyMethod:()V
      16: goto          25 
      19: astore_2 
      20: invokestatic  #3                  // Method finallyMethod:()V
      23: aload_2                        // 从本地变量存入堆栈(抛出的异常,与第19行对应)
      24: athrow                        // 将异常抛出
      25: return
    Exception table:
       from    to  target type
           0     3     9   Class java/lang/Exception
           0     3    19   any
           9    13    19   any
}

先简单介绍几个字节指令,astore_1,即将引用对象从堆栈存入本地变量 1 中,aload_2是将第2个本地变量压入堆栈中,athrow方法是抛出异常的指令 ,即对应于java语言中的throw
分析上面的的字节码内容,发现finallyMethod()方法在字节码中出现了三次,分别在tryMethod后,catchMethod方法后及原来的finally语句块中。即finally语句是通过插入到try与catch语句块中而被执行的(finally语句被称为subroutine)。

现在分析异常表,共三行
1. 如果第0行到第3行(即tryMethod方法)发生了异常,并且异常是Exception类型,则执行第9行(存储异常,执行catchMethod方法)指令。
2. 如果第0行到第3行(即tryMethod方法)发生了异常,异常类型是any,说明是任何类型,因为上面第一条已经匹配了Exception,所以此处是非Exception异常,则执行第19行指令(存储异常,执行finally语句异常)
3. 如是要第9行到第13行(即catchMethod方法)发生了任意类型的异常时,执行第19行指令(即存储异常,执行finally异常)

因此,通过上面的分析,与我们的正常的认知是一样的,即
1. 当 try语句块不发生异常时,直接执行第3行(即finally 语句块)
2. 当try 语句块发生异常时,并且类型是Exception类型时,执行catch 语句块
3. 当try 语句块发生异常时,但是类型没有被catch语句匹配时,直接执行finally语句块的方法
4. 如果发生异常,被catch语句匹配后,如果在catch 语句块中抛出异常,则执行finally语句块的内容
5. 如果try语句发生的异常没有被catch的类型没有匹配到,或者catch语句中发生了异常,则都会执行第19行,即存储异常信息,调用finally语句块的内容,执行完后,通过调用athrow指令,将刚才的异常抛出。

finally语句块对返回值的影响

上面的代码中try 语句块与catch 语句块没有返回值,最后通过第25行的指令return返回,现在在trycatch语句块中增加返回值

public class Test {
    public static void main ( String[] args ) {
        test();
    }

    private static int test () {
        try {
            tryMethod();
            return 0;
        }catch ( Exception e ){
            catchMethod();
            return 1;
        }finally {
            finallyMethod();
            return 2;      // 虽然可以正常编译通过,但是编译器可能会给警告,即不要在finally块中增加return语句
        }       
    }

    private static void finallyMethod () { }

    private static void tryMethod () { }

    private static void catchMethod () { }
}

将代码进行反编译,查看test方法的字节码文件

 private static int test();
    Code:
       0: invokestatic  #3                  // Method tryMethod:()V
       3: iconst_0
       4: istore_0
       5: invokestatic  #4                  // Method finallyMethod:()V
       8: iconst_2
       9: ireturn
      10: astore_0
      11: invokestatic  #6                  // Method catchMethod:()V
      14: iconst_1
      15: istore_1
      16: invokestatic  #4                  // Method finallyMethod:()V
      19: iconst_2
      20: ireturn
      21: astore_2
      22: invokestatic  #4                  // Method finallyMethod:()V
      25: iconst_2
      26: ireturn
    Exception table:
       from    to  target type
           0     5    10   Class java/lang/Exception
           0     5    21   any
          10    16    21   any

第3,4行指令,将常量“0”存入本地变量,第8行,第9行,将常量”2”返回,第14,15行,将常量”1”存入本地变量中,第19,20行,将常量”1”返回,第21行存储try语句或者catch语句产生的异常,第25行,26行,将常量”2”返回。因此,可以发现,不管怎么样,都没有返回常量“0”或者常量“1”,都是返回的常量“2”,即执行了finally中的return语句。
去掉finally语句块中的return语句,做个对比,字节码如下

 private static int test();
    Code:
       0: invokestatic  #3                  // Method tryMethod:()V
       3: iconst_0
       4: istore_0
       5: invokestatic  #4                  // Method finallyMethod:()V
       8: iload_0
       9: ireturn
      10: astore_0
      11: invokestatic  #6                  // Method catchMethod:()V
      14: iconst_1
      15: istore_1
      16: invokestatic  #4                  // Method finallyMethod:()V
      19: iload_1
      20: ireturn
      21: astore_2
      22: invokestatic  #4                  // Method finallyMethod:()V
      25: aload_2
      26: athrow
    Exception table:
       from    to  target type
           0     5    10   Class java/lang/Exception
           0     5    21   any
          10    16    21   any

对比上面的这两块字节码会发现,只有在第8行,第19行,第25行是不一样的,前者分别取的是finally中的返回值,而后者取的是try语句,catch语句的返回值或者抛出发生的异常值。
现在重点来看第26行,前者是ireturn,后者是athrow,即前者是不会再抛出异常的,即如果try语句中发生了没有被catch语句捕获的异常,或者在catch语句块中发生了异常,异常会被无情的吞掉。因此在finally中写return语句,是代码大忌
因此,通过对比就可以发现,在执行try语句块或者catch语句块的return语句之前,都会将返回结果进行存储,在执行完finally语句块后,将刚才的值load出来返回。而如果在finally语句块中增加了return语句的话,返回值是finally语句中的return语句,同时还还可能吞掉异常。

结论

由此,可以得到以下几个结论:
1. 代码增加try-catch语句后,在没有发生异常情况下,不会对性能造成影响(最多相差几条指令,文中例子多了一条goto指令);
2. 执行哪个异常语句是通过查异常表完成的,异常表的顺序是由编译器保证的;
3. finally语句是作为“subroutine”插入到try-catch代码块中执行的;
4. finally语句中增加return语句时,finally语句的返回值会覆盖try或者catch语句块的返回值,并有可能会吞掉异常,代码大忌。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值