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

来源:互联网 发布:godaddy 域名续费优惠 编辑:程序博客网 时间:2024/05/19 15:44

网络上好多文章都分析了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语句块的返回值,并有可能会吞掉异常,代码大忌。

0 0
原创粉丝点击