Java 有趣的程序编译

今天看到一道题,很有意思,特此记录一下。

public class Test {
    public static void main(String args[]) {
        int a = 0;
        int b = 0;
        while(a < 10){
            b = b++;
            a++;
        }
        System.out.println(b);
    }
}

大脑编译一下,直觉告诉我每次循环b都加了两次,但总觉得哪里不对,运行出来发现结果是0,也让我百思不得其解,于是又一顿疯狂搜索学习,最后弄明白了原理,遂写一篇博客,让有兴趣的都可以看看。

上面这个代码用断点debug能看到每次循环b都是0,并没有自增,但断点并不能告诉我们为什么,只有用反汇编的手段才能把它彻底弄明白。不过在看反汇编的代码前,先弄明白什么是局部变量表、什么是操作数栈比较好。

JVM虚拟机作为提供java程序的运行环境,它在运行时,其内存被划分为了几大板块:程序计数器、虚拟机栈、本地方法栈、堆、方法区

我们要了解的局部变量表和操作数栈都是属于虚拟机栈模块,其它模块有兴趣的自行学习吧,我也不甚精通。

虚拟机栈模块中,有一个很重要的数据结构叫做“栈帧”,你可以把它理解为虚拟机栈中的其中一个栈元素,每个栈帧包含了四个东西:局部变量表、操作数栈、动态链接、返回地址

局部变量表用于存储方法中的局部变量和方法中的参数,可以理解为是一个数组结构。

操作数栈作为每条指令的工作区域,指令对数据的操作都要经过操作数栈的入栈出栈来实现。

剩余两个这里就不展开细说了。

上述知识均来自《深入理解Java虚拟机第3版》

有了上面的知识后,直接对编译后的class字节码文件进行javap -c反汇编得到下面的代码。为了方便理解,我对关键部分做了中文注释。如果要具体了解每个指令的用处,请自行搜索“JVM指令集”

Compiled from "Test.java"
public class test.Test {
  public test.Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: iconst_0                          //将0压入操作数栈顶部
       1: istore_1                          //弹出操作数栈顶元素,保存到局部变量表第1个位置
       2: iconst_0                          //将0压入操作数栈顶部
       3: istore_2                          //弹出操作数栈顶元素,保存到局部变量表第2个位置
       4: iload_1                           //将局部变量表第1个位置的值(也就是a的值)压入操作数栈顶部
       5: bipush        10                  //将10压入到操作数栈顶
       7: if_icmpge     21                  //比较操作数栈顶两int型数值大小,当结果等于0(相等)时跳转到21条指令执行,比较完成后清空栈顶两元素
      10: iload_2                           //将局部变量表第2个位置的值(也就是b的值)压入操作数栈顶部
      11: iinc          2, 1                //将局部变量表第2个位置的值(也就是b的值)进行+1操作
      14: istore_2                          //弹出操作数栈顶元素,保存到局部变量表第2个位置(这是关键,这里的0将原先+1后的b给覆盖了,后面每次循环都被0覆盖了)
      15: iinc          1, 1                //将局部变量表第1个位置的值(也就是a的值)进行+1操作
      18: goto          4                   //无条件跳转到第4条指令
      21: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      24: iload_2
      25: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
      28: return
}

可以看到最关键的是第14条指令,每次循环都是先将b的值获取并存放到操作数栈,然后通过iinc指令直接将局部变量表中的b加1,再又将操作数栈中b原先的值0覆盖到局部变量表中的b,所以b最终还是0。

循环部分详解:java中,赋值操作分为两步执行,先将要赋值的数值压入操作数栈顶中(指令就是iconst_xx或者bipush xx,iconst取-1~5,bioush取-128~127),再弹出操作数栈顶元素将其赋值给局部变量表的指定变量(istore_xx)。

而b= b++操作分为两步执行

第一步是b++操作:先通过“iload_2”指令从局部变量表中下标为2的变量b的值从局部变量加载到(此时b=0)操作数栈,然后再通过“iinc 2, 1”指令将局部变量表中的b的值进行+1,此时局部变量表中b的值为1

第二步是b = b操作:赋值操作使用“istore_2"指令是从操作数栈取出栈顶的数值赋给局部变量表中下标为2的变量b。所以b还是被替换为了0。

总的来说就是,b++确实对b进行了自增,但赋值并不是将自增后的b直接覆盖原先的b,而是将b原先的值存到了操作数栈,在b++完成后再取出赋给了b。

所以下次遇到这种赋值操作,一定要想一想赋值是通过操作数栈来完成的。

花了点时间做了一个动态图演示,也把虚拟机栈的结构加了进去,可以参考看下,容易理解。

猜你喜欢

转载自blog.csdn.net/c_o_d_e_/article/details/112283353