字节码角度分析异常处理【try-catch-finally】

有些话、适合烂在心里 提交于 2021-01-28 20:07:33

研究背景:

最近在仔仔细细学习公司的一些项目代码,包含很多模块下的Utils类的阅读,因为在我看来工具类中的绝大部分方法都可以在特定场合下被很多地方调用,那么他的编写可以说是这个模块的精髓代码! 但是发现不少关于try-catch-finally的使用甚至嵌套处理,关于try-catch-finally中return的写法网上也是众说纷纭,记得之前研究JVM底层的时候有涉及过这方面的知识点,但当时没有把这个作为重点去纪录。今天特意拿出来研究研究,探讨一番!

研究主题

  • try-catch-finally的异常处理机制以及返回语句return的编写时机

研究角度

  • 本篇文章我们主要以Java字节码的角度为主Oracle官方文档为辅去权威的理解一下关于异常处理的相关问题

相关环境

  • 我们针对JDK1.8的HotSpot VM虚拟机做讨论
因为在JVM规范约束下,主流选择有:(按流行程度递减)
1. HotSpot VM
2. J9 VM
3. Zing VM

进入正题

先来一段示例代码去简单分析一下

 public static void main(String[] args) {
	int i = 0;
	try {
		i = 10;
	} catch (Exception e) {
		i = 20;
	}
}

简单的try catch 语句返回一个int变量,当然我们这段代码是不会有异常的

那么接下来我们通过Javap反编译,看看这段程序得到的字节码

public static void main(java.lang.String[]);

descriptor: ([Ljava/lang/String;)

flags: ACC_PUBLIC, ACC_STATIC

Code:

	stack=1, locals=3, args_size=1

		0: iconst_0

		1: istore_1

		2: bipush 10

		4: istore_1

		5: goto 12

		8: astore_2

		9: bipush 20

	Exception table:

		from   to   target   type

		2      5     8 	    Class java/lang/Exception

LineNumberTable: ...

LocalVariableTable:

	Start Length Slot Name Signature

		9 3 2 e Ljava/lang/Exception;

		0 13 0 args [Ljava/lang/String;

		2 11 1 i I

		StackMapTable: ...

	MethodParameters: ...

}

解读:
iconst_0 准备的变量0
istore_1 赋值给1号槽位
bipush 10 准备10这个数字
istore_1 赋值给i变量 (可以看到,从第二行开始就已经执行try中的代码了)
goto 12 到第12行 然后执行return 整个语句没有任何问题

但是第8行开始执行的时catch块中的内容,那么他又是怎么进来的呢?

这就是方法中另外一个属性Exception table起的作用了,翻译过来就叫异常表,可以看到他包含四个部分 分别是

  • from to 代表从第二行到第五行的代码(含头不含尾)所以他会检测上面字节码中的2 4 行

  • type检测2-5行代码中是否有异常,如果有是否和我申明的type中的异常匹配

  • target 如果一致就会进入第八行,也就是Java源码的catch块中

      至于第8行的 astore_2 含义就是将异常对象存入Exception table表的e槽位中,相当于就执行到了catch块中,其余解读同理上述。
    

小结:

  • 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号

  • 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置。


再来看看多个 catch 块的情况
	 public static void main(String[] args) {
		int i = 0;
		try {
			i = 10;
		} catch (ArithmeticException e) {
			i = 30;
		} catch (NullPointerException e) {
			i = 40;
		} catch (Exception e) {
			i = 50;
		}
	}

通过Javap反编译,继续得到字节码

public static void main(java.lang.String[]);

	descriptor:([Ljava/lang/String;)
	flags:ACC_PUBLIC,ACC_STATIC
	Code:
	stack=1,locals=3,args_size=1
			0:iconst_0
			1:istore_1
			2:bipush 10
			4:istore_1
			5:goto 26
			8:astore_2
			9:bipush 30
			11:istore_1
			12:goto 26
			15:astore_2
			19:goto 26
			22:astore_2
			23:bipush 50
			25:istore_1
			26:return
	Exception table:
		from to target type
		
		2 5 8 Class java/lang/ArithmeticException
		
		2 5 15 Class java/lang/NullPointerException
		
		2 5 22 Class java/lang/Exception
		
	LineNumberTable:...
	LocalVariableTable:
	
		Start Length  Slot Name Signature
		
		9 3 2 e Ljava/lang/ArithmeticException;
		
		16 3 2 e Ljava/lang/NullPointerException;
		
		23 3 2 e Ljava/lang/Exception; 0 27 0args [Ljava/lang/String;
		
		2 25 1 i I

	StackMapTable:...
	MethodParameters:...

来观看和刚刚有何不同

  • Exception table 中from to 检测2-5行代码是否有异常,有的话分别匹配类型,进入对应的catch块中,将检测到的异常对象放到第二个槽位e变量上然后bipush 一个 30 其余同理。不同点 因为同一时刻只能发生其中一个异常,所以只需要一个槽位就够了所以涉及到槽位的复用

目的就是为了节省栈帧内存开销

小结:

  • 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用

multi-catch 的情况

multi-catch 是Java1.7之后支持的一种新的语法,具体什么意思呢?

代码如下👇

public static void main(String[] args) {
	try {
		Method test = Demo3_11_3.class.getMethod("test");
		test.invoke(null);
	} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
		e.printStackTrace();
	}
}

public static void test() {
	System.out.println("ok");
}

比如我在try块中反射调用方法,此时可能会存在多种类型的异常, 可以用多个catch块来捕获异常, 也可以写一个catch块, 如上述:异常变量e也只需要申明一个,这种语法显然更简洁一些。

那新语法对应的字节码长什么样子呢?继续反编译看看!

public static void main(java.lang.String[]);

	descriptor:([Ljava/lang/String;)
	V flags:ACC_PUBLIC,
	ACC_STATIC
	Code:
		stack=3,locals=2,args_size=1
			0:ldc #2
			2:ldc #3
			4:iconst_0
			5:anewarray #4
			8:invokevirtual #5
			11:astore_1
			12:aload_1
			15:anewarray #6
			18:invokevirtual #7
			21:pop
			22:goto 30
			25:astore_1
			26:aload_1
			27:invokevirtual #11 // e.printStackTrace:()
			30: return
	Exception table:
		from to target  type
		0   22  25      Class java/lang/NoSuchMethodException
		0   22  25      Class java/lang/IllegalAccessException
		0   22  25      Class java/lang/reflect/InvocationTargetException
	LineNumberTable: ...
	LocalVariableTable:
		Start Length   Slot Name Signature
		12      10      1   test Ljava/lang/reflect/Method;
		26      4       1   e    Ljava/lang/ReflectiveOperationException;
		0       31      0   args [Ljava/lang/String;
	StackMapTable: ...
	MethodParameters: ...

主要关注catch块的Exception table有何不同

①检测0-22行(含头不含尾)这段代码的运行
②type 捕获异常类型
③target  25 不管哪种异常都会进入25行位置处
④astore_1 将异常对象的引用存储到1号槽位
⑤aload_1 将1号槽位的引用对象加载到栈内存中

小结: 这样看来没什么不同,只是将多个异常对象的入口做统一


👇接下来看finally块,finally相对比较复杂。

public static void main(String[] args) {
	int i = 0;
	try {
		i = 10;
	} catch (Exception e) {
		i = 20;
	} finally {
		i = 30;
	}
}

反编译后的字节码👇

public static void main(java.lang.String[]);

	descriptor:([Ljava/lang/String;)
	flags:ACC_PUBLIC,ACC_STATIC
	Code:
		stack=1,locals=4,args_size=1
			0:iconst_0
			1:istore_1 // 0 -> i
			2: bipush 10 // try --------------------------------------
			4: istore_1 // 10 -> i                                                    |
			5: bipush 30 // finally                                                  |
			7: istore_1 // 30 -> i                                                    |
			8: goto 27 // return -----------------------------------
			11: astore_2 // catch Exceptin -> e ----------------------
			15:bipush 30 // finally                                                    |
			17: istore_1 // 30 -> i                                                     |
			18: goto 27 // return -----------------------------------
			21: astore_3 // catch any -> slot 3 ----------------------
			22: bipush 30 // finally                                                  |
			24: istore_1 // 30 -> i                                                    |
			25: aload_3 // <- slot 3                                                 |
			26: athrow // throw ------------------------------------
			27: return
	Exception table:
		from to target type
		2 5 11 Class java/lang/Exception
		2 5 21 any // 剩余的异常类型,比如 Error
		11 15 21 any // 剩余的异常类型,比如 Error LineNumberTable: ... LocalVariableTable: Start Length Slot Name Signature
		12 3 2 e Ljava/lang/Exception; 0 28 0 args [Ljava/lang/String;
		2 26 1 i I 
		StackMapTable: ... 
	MethodParameters: ...

!!来看try块中的内容做了什么事情

  1. istore_1 // 0 -> i 创建i变量
  2. bipush 10 创建10这个值
  3. istore_1 // 10 -> i 将10赋值给i变量
  4. bipush 30 // finally
  5. istore_1 // 30 -> i 将30赋值给i
  6. goto 27 // return 返回语句

为什么呢?try中只有对i的赋值操作,为什么有finally的代码呢?

答案:实际上就是finally的工作原理会分别将finally块中的代码分别放入try和catch的分支中。

(*本质是将finally块中的代码复制到try中,以此来保证try后的finally一定会被执行)

为了验证是否是这样,可以自行观看catch块中的字节码!

那这样的话21-26的代码并没有被解释,接下来一起看看!

在有些情况下Exception并不能捕获所有异常,例如和他同级或者是他的父类的异常,这种情况下是捕获不到的比如Error、Throwable 等。所以这种情况下是和catch捕获的异常匹配不到的,而finally又是无论如何要被执行的。

所以Exception table:中多了两项

第一块检测2-5 try中的代码、第二块检测11-15catch 中的代码

此时如果try中出现catch的同级或父级的异常又或者是catch中出现异常,就会跳到21行 finally 块中执行。以此来达到目的。

finally 后catch中的异常还是要被抛出去的,所以继续执行

  • aload_3 // <- slot 3 的操作

  • athrow 继续抛出异常对象

     所以25  26 行算另外一个分支
    

小结:这段其实有三个分支

  1. try分支
  2. catch分支
  3. catch没有匹配到的其他异常的分支

finally原理:可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程。


finally 被拿来当面试题已经见惯不惯了,一起看几个典型案例!

Java代码👇
public static void main(String[] args) {
	int result = test();
	System.out.println(result);
}
答案:20
public static int test() {
	try {
		return 10;
	} finally {
		return 20;
	}
}
字节码👇
 public static int test();

	descriptor:()
	I flags:ACC_PUBLIC,ACC_STATIC
	Code:
	stack=1,locals=2,args_size=0
			0:bipush 10 // <- 10 放入栈顶
			5:ireturn // 返回栈顶 int(20)
			6: astore_1 // catch any -> slot 1
			7: bipush 20 // <- 20 放入栈顶
			9: ireturn // 返回栈顶 int(20)
	Exception table:
		from to target type
		0    3   6     any
	LineNumberTable: ... 
	StackMapTable: ...
  • 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准
  • 至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子
  • 跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常

*所以建议一定不要在finally中些return语句!

	public static void main(String[] args) {
		int result = test();
		System.out.println(result);
	}

	public static int test() {
		try {
			int i = 1 / 0;
			return 10;
		} finally {
			return 20;
		}
	}

finally 对返回值影响

java代码👇
public static void main(String[] args) {
	int result = test();
	System.out.println(result);
}

public static int test() {
	int i = 10;
	try {
		return i;
	} finally {
		i = 20;
	}
}
字节码👇
 flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=0
        0: bipush 10 // <- 10 放入栈顶
        2: istore_0 // 10 -> i
        3: iload_0 // <- i(10)
        4: istore_1 // 10 -> slot 1,暂存至 slot 1,目的是为了固定返回值
        5: bipush 20 // <- 20 放入栈顶
        7: istore_0 // 20 -> i
        8: iload_1 // <- slot 1(10) 载入 slot 1 暂存的值
        9: ireturn // 返回栈顶的 int(10)
        10: astore_2
        11: bipush 20
        13: istore_0
        14: aload_2
        15: athrow
Exception table:
    from to target type
    3    5  10     any
LineNumberTable: ...
LocalVariableTable:
     Start Length Slot Name Signature 
      3      13    0   i     I 
    StackMapTable: ...
所以在finally不带return就不会吞掉,而回athrow回去!

总结:

我们发现在try中return了,又在finally 中操作了变量,他是并不会影响到返回结果的,因为他在return的时候做了一个暂存操作,然后执行了finally中的代码,最后将暂存的这个变量返回!所以结果已经在try中的return时固定了,之后finally中修改已经无效了!


对于网上的一些文章有的的确很详细很不错,但有些也很假,对于之前参考过的一篇不错的文章希望趁热打铁去看看 https://my.oschina.net/lixingsikao/blog/4927595

以上就是在最权威的字节码角度去分析异常处理的相关执行原理。


希望同仁志士,前来参考以及指点!共同进步,发扬文化精神!转载请标明出处!

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!