从反编译的角度浅谈JVM如何执行i++和++i操作
之前对于i++和++i的理解比较浅显。通常,对i++的表述为先赋值再自增,对于++i的表述为先自增再赋值。鉴于在学习JUC的原子变量时,对i++的原子性问题不能完全理解,并且在最近的笔试中,发现其有很多玩法。因此,对于JVM如何执行这两个操作有必要研究一下。对小问题的探究有助于加深对java理解。
注意:以下i均以整型为例!
一、 《疯狂Java讲义》中关于++运算符的描述
《疯狂Java讲义》(第4版)中对于++运算符的描述如下:
++:自加。该运算符有两个要点:1.自加是单目运算符,只能操作一个操作数;2.自加运算符只能操作单个数值型(整型、浮点型都行)的变量,不能操作常量或表达式。运算符既可以出现在操作数的左边,也可以出现在操作数的右边。但出现在左边和右边的效果是不一样的。如果把++放在左边,则先把操作数加1,然后才把操作数放入表达式中运算;如果把++放在右边,则先把操作数放入表达式中运算,然后才把操作数加1。看如下代码:
int a = 5;
// 让a先执行算术运算,然后自加
int b = a++ + 6;
// 输出a的值为6,b的值为11
System.out.println(a + "\n" +b);
执行完后,a的值为6,而b的值为11。当++在操作数右边时,先执行a + 6的运算(此时a的值为5),然后对a加1。对比下面代码:
int a = 5;
// 让a先自加,然后执行算术运算
int b = ++a + 6;
// 输出a的值为6,b的值为12
System.out.println(a + "\n" +b);
执行的结果是a的值为6,b的值为12。当++在操作数左边时,先对a加+,然后执行a + 6的运算(此时a的值为6),因此b为12.
二、 局部变量表和操作数栈
对i的运算是在虚拟机栈中进行的。首先,介绍虚拟机栈中与i的运算有关的两个概念:局部变量表和操作数栈。
局部变量表(Local Variable Table)
在编译程序代码的时候就可以确定栈帧中需要多大的局部变量表,具体大小可在编译后的 Class 文件中看到。局部变量表的容量以 Variable Slot(变量槽)为最小单位,每个变量槽都可以存储 32位长度的内存空间。在方法执行时,虚拟机使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法,那局部变量表中第 0位索引的 Slot 默认是用于传递方法所属对象实例的引用(在方法中可以通过关键字 this来访问到这个隐含的参数)。其余参数则按照参数表顺序排列,占用从 1 开始的局部变量 Slot。基本类型数据以及引用和returnAddress(返回地址)占用一个变量槽,long 和 double 需要两个。
操作数栈(Operand Stack)
同样也可以在编译期确定大小。 Frame 被创建时,操作栈是空的。操作栈的每个项可以存放 JVM 的各种类型数据,其中 long 和 double 类型(64位数据)占用两个栈深。 方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作(与Java 栈中栈帧操作类似)。操作栈调用其它有返回结果的方法时,会把结果 push 到栈上(通过操作数栈来进行参数传递)。
三、JVM如何执行++运算符
下面通过四个用例介绍jvm是如何执行i++和++i操作的。
1、i = i++;问题
以下面代码为例:
public class AutoIncrement {
public static void main(String[] args) {
int i = 1;
i = i++;
System.out.println(i);
}
}
输出结果为:1。
我们来反编译下,看看JVM是如何执行这段代码的操作的。
反编译结果如下:
从反编译后的结果可以看出,int i = 1;
是如下两步操作,即:
0: iconst_1 —— 把常量1压入操作数栈;
1: istore_1 —— 把栈顶元素1存入局部变量表的第一个变量i中;
i = i++;
是如下三步操作:
2: iload_1 —— 把第一个变量i的值(即i的值1)压入栈中;
3: iinc 1, 1 —— 第一个变量i的值加1(此时变量i的值为2);
6: istore_1 —— 把栈顶元素1存入到第一个变量i中(此时变量i的值又为1)。
通过以上不难看出,i = i++;
的操作实际上分为三个步骤“读-改-写”(涉及i++的原子性问题),可以理解为:
int temp = i;
i = i + 1;
i = temp;
2、i = i++ + i;问题
以下面代码为例:
public class AutoIncrement {
public static void main(String[] args) {
int i = 1;
i = i++ + i;
System.out.println(i);
}
}
输出结果为:3。
我们来反编译下,看看JVM是如何执行这段代码的操作的。
反编译结果如下:
从反编译后的结果可以看出,i = i++ + i;
是如下五步操作:
2: iload_1 —— 把第一个变量i的值(即i的值1)压入栈中;
3: iinc 1, 1 —— 第一个变量i的值加1(此时变量i的值为2);
6:iload_1 —— 把第一个变量i的值(即i的值2)压入栈。
7: iadd —— 取出栈中的top元素和top-1元素进行相加得3,并将结果压入栈中;
8: istore_1 —— 把栈顶元素3存入到第一个变量i中(此时变量i的值为3)。
3、i = ++i;问题
以下面代码为例:
public class AutoIncrement {
public static void main(String[] args) {
int i = 1;
i = ++i;
System.out.println(i);
}
}
输出结果为:2。
我们来反编译下,看看JVM是如何执行这段代码的操作的。
反编译结果如下:
从反编译后的结果可以看出,i = ++i;
是如下三步操作:
2: iinc 1, 1 —— 第一个变量i的值加1(此时变量i的值为2);
5: iload_1 —— 把第一个变量i的值(即i的值2)压入栈;
6: istore_1 —— 把栈顶元素2存入到第一个变量i中(此时变量i的值为2)。
以上操作可以理解为:
i = i + 1;
int temp = i;
i = temp;
4、i = ++i + i;问题
以下面代码为例:
public class AutoIncrement {
public static void main(String[] args) {
int i = 1;
i = ++i + i;
System.out.println(i);
}
}
输出结果为:4。
我们来反编译下,看看JVM是如何执行这段代码的操作的。
反编译结果如下:
从反编译后的结果可以看出,i = ++i + i;
是如下五步操作:
2: iinc 1, 1 —— 第一个变量i的值加1(此时变量i的值为2);
5: iload_1 —— 把第一个变量的i值(即i的值2)压入栈;
6:iload_1 —— 把第一个变量的i值(即i的值2)压入栈。
7: iadd —— 取出栈中的top元素和top-1元素进行相加得4,并将结果压入栈中;
8: istore_1 —— 把栈顶元素4存入到第一个变量i中(此时变量i的值为4)。
四、总结
通过以上四个用例,我们不难发现,jvm在执行i++时是先将局部变量表中变量i(以i=1为例)的值先压入操作数栈中,然后再将局部变量表中变量i值加1,这时i的值为2。而整个i++用于运算的是已经压入操作数栈中的1,即使用自加前i的值。若i++之后有关于i的运算,如++i + i
,再将局部变量表中的变量i(此时i的值为2)压入操作数栈中,即使用自加后i的值。而jvm在执行++i时(同样以i=1为例),是先将局部变量表中的变量i值加1。此时i的值为2。然后再将变量i的值压入操作数栈中。这时整个++i操作用于运算的是已经完成自加1操作的值。若++i之后有关于i的运算,如i++ + i
,i同样使用的是已经完成自加的值(i=2)。
通过以上分析,对jvm如何执行++操作有了更深的理解。在今后的工作或笔试中遇到类似i = i++ + i + i
、i = i++ + ++i + i
等问题都知道如何应对了。
本文地址:https://blog.csdn.net/m0_47503416/article/details/109921158