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

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."":()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技术 有趣的java程序_操作数