finally知多少(二)

接:finally知多少(一)

问题解释

结合《深入Java虚拟机(第二版)》这本书和代码编译后产生的二进制指令代码,我对以上问题做了部分解释,鉴于我的才疏学浅,有些观点是有误的,希望高手指正(有误的观点容易引起误导,这也是所以我一直非常小心,奈何水平有限,有些时候难免出错)。

 

在《深入Java虚拟机(第二版)》的第18章中提到,在早期的Java中,finally的行为是通过JSR指令来实现的,并且为这个指令引入了微型子程序的概念。我的理解,所谓微型子程序就是在函数A中嵌入一个不完整的函数B的调用。比如在这本书上的一个例子:

	private static int microSubroutine(boolean bValue) {
		try {
			if(bValue) {
				return 1;
			}
			return 0;
		} finally {
			System.out.println("finally");
		}
	}

 

会生成以下的二进制代码:

 0 iload_0

 1 ifeq 11

 4 iconst_1

 5 istore_1

 6 jsr 24

 9 iload_1

10 ireturn

11 iconst_0

12 istore_1

13 jsr 24

16 iload_1

17 ireturn

18 astore_2

19 jsr 24

22 aload_2

23 athrow

 

24 astore_3

25 getstatic #7 <Field java.io.PrintStream out>

28 ldc #1 <String “finally”>

30 invokevirtual #8 <Method void println(java.lang.String)>

33 ret 3

 

如上,24前缀的代码行以后的部分就是微型子程序,在每一个出口之前都会用JSR调用这个微型子例程序,在这个微型子例程序返回(ret)后,返回调用JSR指令的下一条指令,然后返回(ireturnathrow)。

jsr指令和ret指令的格式如下:

jsr    branchbyte1, branchbyte2

把返回地址压栈,跳转至((branchbyte1<<8) | branchbyte2)的位置继续之行。

ret index

返回在index指示的局部变量中存储的值(位置)。

 

在上面的二进制代码中,每次通过jsr 24跳转到微型子程序,它先将返回地址(jsr 24指令的下一条指令的地址)保存在index3的局部变量中,执行完微型子程序后,通过ret 3返回到调用jsr 24指令的下一条指令执行,并最终执行返回。

 

可是后来(有人说是自1.4.2后),JVM中取消了jsr指令了,所有finally内部的代码都内联到源代码中了(二进制的源代码)。所以以上的代码在之后的编译器中会产生如下的二进制代码:

     0  iload_0 [bValue]

     1  ifeq 14

     4  getstatic java.lang.System.out : java.io.PrintStream [16]

     7  ldc <String "finally"> [94]

     9  invokevirtual java.io.PrintStream.println(java.lang.String) : void [24]

    12  iconst_1

13  ireturn

 

    14  getstatic java.lang.System.out : java.io.PrintStream [16]

    17  ldc <String "finally"> [94]

    19  invokevirtual java.io.PrintStream.println(java.lang.String) : void [24]

    22  iconst_0

23  ireturn

 

    24  astore_1

    25  getstatic java.lang.System.out : java.io.PrintStream [16]

    28  ldc <String "finally"> [94]

    30  invokevirtual java.io.PrintStream.println(java.lang.String) : void [24]

    33  aload_1

    34  athrow

 

额,貌似有点偏题了,以上的描述是为了解释《深入Java虚拟机(第二版)》中对finally描述过时的描述。下面让我们来真正的解决这个问题。还是从生成的Java二进制代码入手。

 

首先来看一下valueChangeInFinallyTest()函数的二进制代码(注释了打印语句,使代码简洁):

         //int i = 10

     0  bipush 10

     2  istore_0 [i]

       //int j = 1

     3  iconst_1

     4  istore_1 [j]

       //i = 100

     5  bipush 100

     7  istore_0 [i]

       //j = 2

     8  iconst_2

     9  istore_1 [j]

       //保存i的值,因为它是要返回的

    10  iload_0 [i]

11  istore 4

//--------------------------------内联finally语句块(开始)----------------------

//i = 1000

    13  sipush 1000

16  istore_0 [i]

//j = 3

    17  iconst_3

18  istore_1 [j]

//--------------------------------内联finally语句块(结束)----------------------

//加载保存后的i的值,并返回。这里返回的是finally语句块执行前的i(由istore 4语句缓存起来)的值,因而在finally语句块中任何对i的操作并不会保留下来。这是在没有异常发生的情况下。

    19  iload 4

21  ireturn

 

    22  astore_2 [e]

    23  aload_2 [e]

24  invokevirtual java.lang.Exception.printStackTrace() : void [104]

//--------------------------------内联finally语句块(开始)----------------------

    27  sipush 1000

    30  istore_0 [i]

    31  iconst_3

32  istore_1 [j]

//--------------------------------内联finally语句块(结束)----------------------

33  goto 45

 

36  astore_3

//--------------------------------内联finally语句块(开始)----------------------

    37  sipush 1000

    40  istore_0 [i]

    41  iconst_3

42  istore_1 [j]

//--------------------------------内联finally语句块(结束)----------------------

//而在异常发生但没有被正确处理的情况下,返回值已经没有什么意义了。

    43  aload_3

44  athrow

 

//这里是在有异常发生,并且异常得到了正确处理的情况下返回的,此时在finally语句块中对i的操作就会保存下来,并返回给调用者。

    45  iload_0 [i]

    46  ireturn

相信以上的注释已经能很好的的解决这个问题了(注:这里j的存在是为了证明在内联finally语句块的时候,它只缓存返回值i,而无须缓存其他变量的值,如j的值)。需要特别注意的一点是,如果正常返回的话,finally语句块中修改i的值是保存不下来的,但是如果出现异常,并被正常捕获后,在finally语句块中修改的i的值就会保存下来了。

 

那么对valueChangeReturnInFinallyTest()函数中的现象如何解释呢?对这个问题解释,首先要理解ireturn的指令。ireturn指令没有操作数,它把当前操作栈的栈顶的int值作为默认的操作数。ireturn指令会弹出当前栈顶的int值,将其压入调用者的操作栈中,同时忽略当前操作栈中的其他值,即函数正常返回。因而如果在不优化的情况下,在finally语句块中的return语句会返回当前栈顶的int值(修改后的i值),然后函数返回,此时栈上的其他操作数就被忽略了,并且原本应该执行的ireturn语句也不会之行了。这种方式甚至会忽略抛出的异常,即使当前方法有异常抛出,它的调用方法还是认为它正常返回了。

如果查看优化后的valueChangeReturnInFinallyTest()方法的二进制源码后,会发现当前的代码更加简洁了。但是它还是没有避免在finally语句块中使用return后,会忽略没有捕获到的异常的问题。

         //int i = 10

     0  bipush 10

     2  istore_0 [i]

       //int j = 1

     3  iconst_1

     4  istore_1 [j]

       //i = 100

     5  bipush 100

     7  istore_0 [i]

       //j = 2

     8  iconst_2

     9  istore_1 [j]

10  goto 22

//catch block

    13  astore_2 [e]

    14  aload_2 [e]

    15  invokevirtual java.lang.Exception.printStackTrace() : void [104]

    18  goto 22

21  pop

//--------------------------------内联finally语句块(开始)----------------------

//i = 100

    22  sipush 1000

25  istore_0 [i]

//j = 3

    26  iconst_3

27  istore_1 [j]

//--------------------------------内联finally语句块(结束)----------------------

//返回finally语句块中i的值

    28  iload_0 [i]

    29  ireturn

经过以上的解释,我想对refValueChangeInFinallyTest()函数中的现象就比较好解释了,因为当进入finally语句块的时候,保存的只是Person实例的一个引用,在finally语句块中依然可以通过引用操作Person内部成员的,因而在finally语句块中的修改才能保存下来。

 

而进过编译器优化后的finallyReturnTest()finallyBreakTest()函数生成的二进制代码就成一样的了:

     0  iload_0 [value]

     1  ifeq 8

     4  goto 8

     7  pop

     8  iconst_0

     9  ireturn

 

后记

原本以为这是一个小问题的,没想到花了我一个下午的时间才把问题说清楚了,而在描述问题的过程中,我对问题的本质也看的更加清晰了。这个问题开始是源于我在论坛http://www.iteye.com/topic/458668中看到,感觉论坛里面的人都没很好的说清楚这个问题,刚好我看完了《深入Java虚拟机(第二版)》的书,就把这个问题完整的描述出来了。

你可能感兴趣的:(java,jvm,虚拟机,J#)