网络上好多文章都分析了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行。因此两者的差异仅在于innerTry
比outerTry
多执行了一条1个字节长度的goto
指令,因此两者的性能可以认为是没有差异的。
小结
通过上面分析可以看到,两者差别仅在于一条goto
语句,所以可以认为两者的性能是没有差异的
try-catch语句的实现机理
新的问题来了,我们的代码及反编译代码中都有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
语句生成异常表。当发生异常时,查询异常表是根据顺序进行的,因此,大家就理解,为什么如果有多个异常的时候,需要把先想要匹配的放在前面。如果在这个异常表中指不到相应的处理代码。
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
返回,现在在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
语句块的返回值,并有可能会吞掉异常,代码大忌。