zoukankan      html  css  js  c++  java
  • 通过指令码来判断Java代码的执行顺序(++问题与return和finally的问题)

    问题

    在《深入理解Java虚拟机》一书中遇到了如下代码:

    public int method() {
    	int i;
    	try {
    		i = 1;
    		return i;
    	} catch (Exception e) {
    		i = 2;
    		return i;
    	} finally {
    		i = 3;
    	}
    }
    

    由于曾经搜了一下return和finally的问题后,只是简单的看到了finally会执行,从而导致自己误以为只是简单地把finally的执行顺序放到return语句之前,因此判断这段代码的执行结果应该是3,可实际运行结果是1。研究后发现自己当初真是太糊涂,于是便记录下来。

    工具

    我们都知道,class文件中的内容就是可供JVM理解的字节码,JVM也是根据class的字节码来执行程序代码,所以class文件中就包含着程序代码最终的执行顺序。

    我们可以通过官方提供的javap -c 再加上class文件的路径来得到各个方法对应的指令码。

    例如:javap -c Test.class

    引例

    由于是打算使用JVM的指令码来解决这个问题,刚开始先以一个简单的方法来说明一下。对于如下方法:

    public int method1() {
    	int i = 1;
    	return i;
    }
    

    该方法对应的指令码为:

    public int method1();
        Code:
           0: iconst_1
           1: istore_1
           2: iload_1
           3: ireturn
    

    每个指令对应着一个操作,上面的指令码意思是:

    1. 将int型数值0推送至栈顶
    2. 将栈顶int型元素存入第二个空间中
    3. 将第二个空间的int型元素推送至栈顶
    4. 返回将栈顶的int型元素并退出这个方法

    由此可以看出,通过指令码,我们可以直观地看到程序代码的执行顺序,这对于解决任何执行顺序的问题是一个利器。

    如果还是感觉有些不明所以,那我们可以再看看i++++i的问题。对于如下代码:

    // return 1
    public int method2() {
    	int i = 1;
    	return i++;
    }
    
    // return 2
    public int method3() {
    	int i = 1;
    	return ++i;
    }
    

    它们的指令码分别是:

    public int method2();
        Code:
           0: iconst_1
           1: istore_1
           2: iload_1
           3: iinc          1, 1
           6: ireturn
    
      public int method3();
        Code:
           0: iconst_1
           1: istore_1
           2: iinc          1, 1
           5: iload_1
           6: ireturn
    

    显然,这两段指令码最大的区别就是iinc 1,1指令的位置不同,而且如果把这条指令删除,那么与method1的指令码完全一致,对应源代码来看,这条指令就是++这个符号的影响了。

    而这个关键的iinc 1,1指令的作用哪怕完全不懂也能猜出来,就是将第二个空间的int数据+1后再放回第二个空间

    将这个含义放到指令码中再重新捋一遍,以method2为例:

    1. 将int型数值0推送至栈顶
    2. 将栈顶int型元素存入第二个空间中
    3. 将第二个空间的int型元素(1)推送至栈顶
    4. 将第二个空间的int数据+1后再放回第二个空间
    5. 返回将栈顶的int型元素并退出这个方法

    需要注意的是,第三步是将1而不是整个空间推送至栈顶,所以第四步对第二个空间中的数据1加1后并没有改变栈顶的值,因此返回值为1。相对的,method2则是:

    1. 将int型数值0推送至栈顶
    2. 将栈顶int型元素存入第二个空间中
    3. 将第二个空间的int数据+1后再放回第二个空间
    4. 将第二个空间的int型元素(2)推送至栈顶
    5. 返回将栈顶的int型元素并退出这个方法

    所以,返回的是2。

    解决

    现在我们可以看最初的method方法了,在这里再复制一遍代码:

    public int method() {
    	int i;
    	try {
    		i = 1;
    		return i;
    	} catch (Exception e) {
    		i = 2;
    		return i;
    	} finally {
    		i = 3;
    	}
    }
    

    对应的指令码:

    public int method();
        Code:
           0: iconst_1
           1: istore_1
           2: iload_1
           3: istore        4
           5: iconst_3
           6: istore_1
           7: iload         4
           9: ireturn
          10: astore_2
          11: iconst_2
          12: istore_1
          13: iload_1
          14: istore        4
          16: iconst_3
          17: istore_1
          18: iload         4
          20: ireturn
          21: astore_3
          22: iconst_3
          23: istore_1
          24: aload_3
          25: athrow
        Exception table:
           from    to  target type
               0     5    10   Class java/lang/Exception
               0     5    21   any
              10    16    21   any
    

    这段指令码不同的地方在于最后有一个异常表,我们先不用管它,先看到第一个ireturn指令的指令码,即代码中的第9行为止的指令码:

    0: iconst_1
    1: istore_1
    2: iload_1
    3: istore        4
    5: iconst_3
    6: istore_1
    7: iload         4
    9: ireturn
    

    这段指令码就是当没有异常时,程序执行的指令码,finally语句块的指令码已经包含在里面了:

    1. 将int型数值1推送至栈顶
    2. 将栈顶int型元素存入第二个空间中
    3. 将第二个空间的int型元素(1)推送至栈顶
    4. 将栈顶int型元素存入第五个空间中
    5. 将int型数值3推送至栈顶
    6. 将栈顶int型元素存入第二个空间中(3
    7. 第五个空间的int型元素(1)推送至栈顶
    8. 返回将栈顶的int型元素并退出这个方法

    由此可以看出,方法返回的是第五个空间的1而不是第二个空间的3,和运行结果一致。

    其中,关键的地方就是第四步以及第七步。由此可见,Java程序在执行时遇到return语句时,会先将方法的返回值保存起来,如果还有finally语句块,那么就先执行finally语句块,最后再将返回值取出后返回

    另外,如果return后跟的是表达式或者方法,那么会先计算出最终的返回值后再执行finally语句块,可自行验证。

    当然,如果保存的返回值是一个引用类型的变量,那么在finally代码块中修改则会改变这个变量本身的属性,因而改变返回值的属性,毕竟finally的代码是的的确确执行过了。

    例如,返回一个List,在finally中又对List进行了增加或删除,那么返回的List的内容自然也变了。

    附加

    关于指令码其余的部分,涉及到更多知识,在这里根据我的理解简单说一下。

    这段指令码最后有一个异常表,它的含义可以简单解释为:在[from,to)的区间内,如果发生type类型的异常,那么就跳到target执行。

    正因为有了异常表的存在,在出现异常时,程序可以根据产生的异常来跳到正确的位置执行接下来的代码。

    [10,20]即为catch代码块对应的指令码,不过其中会把捕捉到的异常存储下来,也就是源代码中的Exception e。[21,25]则是会把try语句块中抛出的catch没有捕捉的异常保存下来,然后执行finally的代码,最后抛出该异常结束方法。

    这三片指令码都包含了finally的指令码,也就保证了源代码中finally的代码肯定会执行。

    结论

    Java程序在执行时遇到return语句时,会先将方法的返回值保存起来,如果还有finally语句块,那么就先执行finally语句块,最后再将返回值取出后返回。另外,如果return后跟的是表达式或者方法,那么会先计算出最终的返回值后再执行finally语句块。

    笔记内容只是本人思考而写,如果有什么问题,还请指出,谢谢!

  • 相关阅读:
    随手
    会使用基本的Render函数后,就会想,这怎么用 v-for/v-if/v-model;我写个vue Render函数进阶
    iframe子页面与父页面元素的访问以及js变量的访问[zhuan]
    vue element-ui 的奇怪组件el-switch
    URLSearchParams和axios的post请求(防忘记)
    ios vue2.0使用html5中的audio标签不能播放音乐
    Unity Input System教程
    关于OpenGPU.org
    Better ultra_simple for Slamtec RPLIDAR on Linux
    八字心得
  • 原文地址:https://www.cnblogs.com/baka-sky/p/8354641.html
Copyright © 2011-2022 走看看