网络上好多文章都分析了java语言try-catch-finally语句(下文中称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行。因此两者的差异仅在于innerTry比outerTry多执行了一条1个字节长度的goto指令,因此两者的性能可以认为是没有差异的。
通过上面分析可以看到,两者差别仅在于一条goto语句,所以可以认为两者的性能是没有差异的
新的问题来了,我们的代码及反编译代码中都有catch语句,那么catch语句是如何被找到的呢? java对try语句的执行是通过异常表进行的。 看innerTry与outerTry反编译下面都有如下所示的反编译代码
Exception table: from to target type 17 20 23 Class java/lang/Exception这个就是异常表,上面的异常表来自于innerTry方法。 异常表有四部分组成,from,to,target,type。from表示从哪个字节指令开始,to表示到哪个字节指令结束(不包含),target表示如果发生异常了,下一步应该执行哪个指令,type表示匹配的异常,如果这个异常没有匹配上,是不会执行target地方的指令的。因此,上面的异常表表明,从第17行指令开始,到第20行指令(不包含)结束(正好是tryMethod),如果发生Exception类型的异常,就去执行第23行的代码,而第23行的代码是astore(将异常对象压入栈),第25行代码是执行catchMethod方法。 代码进行编译时,编译器根据try语句生成异常表。当发生异常时,查询异常表是根据顺序进行的,因此,大家就理解,为什么如果有多个异常的时候,需要把先想要匹配的放在前面。如果在这个异常表中指不到相应的处理代码。
我们来看下面的代码
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指令,将刚才的异常抛出。
上面的代码中try 语句块与catch 语句块没有返回值,最后通过第25行的指令return返回,现在在try与catch语句块中增加返回值
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语句块的返回值,并有可能会吞掉异常,代码大忌。
