从jvm字节码指令看i=i++和i=++i的区别
1. 场景的产生
先来看下下面代码展示的两个场景
@Test
void testIPP() {
int i = 0;
for (int j = 0; j < 10; j++) {
i = i++;
System.out.println(i);
}
}
@Test
void testPPI() {
int i = 0;
for (int j = 0; j < 10; j++) {
i = ++i;
System.out.println(i);
}
}
首先两个方法的开始都定义了一个int变量 i,并初始化为0,testIPP()方法里进行了一个for循环,循环内容为对i赋值,i=i++,并输出当前循环下的i的值,testPPI()方法里循环同样是对i赋值,但是为i=++i。 这两个方法一看很简单,不就是对每循环一次对变量 i 进行一次+1操作并打印i值吗,但是问题没有这么简单,可以先猜测一下这两个方法的打印结果。
实际打印结果:
testIPP():
0
0
0
0
0
0
0
0
0
0
testPPI():
1
2
3
4
5
6
7
8
9
10
可以看到testIPP方法里每次循环打印的都是0,这是与你想的不太一样,按道理结果应该是跟testPPI()一样的,从1打印到10,这是为什么呢,我们需要从JVM的字节码层面来看看这两个方法底层到底是怎么执行的。
2. 前置知识
jvm字节码指令
iconst_1: 将常量1压入操作数栈中,即栈顶位置。
istore_1 : 将当前操作数栈栈顶元素放到局部变量表中slot槽位置为1的地方.
iload_1: 将局部变量表中slot槽位置为1的变量压入操作数栈,即栈顶位置。
iinc 1 by 1: 将局部变量表中slot槽位置为1的变量进行自增操作,自增的数值为后面的1.
这里只解释下涉及的几个重要的字节码指令,其余的字节码指令暂时先不做解释。
3. 字节码分析
要想获得字节码指令文件,通常有两种方法,1是先将.java源文件编译为.class文件,然后再借助jdk提供的javap 反解析工具将.class文件解析成jvm执行的字节码指令,2是通过jclasslib 插件来查看字节码指令,可以在IDEA的插件商城中下载jclasslib 插件,编译.java文件后就能通过插件查看了,非常方便。
首先来看testIPP()的局部变量表:

可以看到,在局部变量表slot槽为0的位置存放的当前对象的引用this,1的位置为变量i,j的位置为变量2.因为这是非静态方法,所以调用该方法时肯定是已经创建了实例,在jvm中所有非静态方法的栈帧中的局部变量表的第0个位置都会默认放置this.
接下来看核心的字节码指令
0 iconst_0 //将常量0压入操作数栈
1 istore_1 //将操作数栈元素放到局部变量表位置1的地方,即i=0
2 iconst_0 //将常量0压入操作数栈
3 istore_2 //将操作栈顶元素放到局部变量表位置2的地方,即j=0
4 iload_2 //将局部变量表2的位置的元素取出压入操作数栈,即取出j
5 bipush 10 //将10从字节转为int型并压入操作数栈
7 if_icmpge 28 (+21) //比较操作数栈中两者的大小,即for循环的判断 i<10
10 iload_1 //此处就进入到for循环内部,从局部变量表1的位置取出压入操作数栈,即取出i,此时i=0
11 iinc 1 by 1 //对局部变量表1的位置的元素进行自增+1,即i++; 注意到实际上此时i的值已经是1了
14 istore_1 //将操作数栈顶元素放入局部变量表中位置1的地方,此时操作数栈顶为0,相当于i=0,又把前面自增的值给覆盖了
15 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;> //获取静态类System.out
18 iload_1 //将局部变量表位置1的值压入操作数栈顶,即i
19 invokevirtual #3 <java/io/PrintStream.println : (I)V> //调用println打印i,所以每次循环一直都打印0
22 iinc 2 by 1
25 goto 4 (-21)
28 return
再来看下testPPI()的局部变量表:
可以看到跟testIPP()的位置都是一样的,没什么变化。

再来看看字节码指令:
可以看到前面的部分都是没有变化的,主要来看for循环的不同
0 iconst_0
1 istore_1
2 iconst_0
3 istore_2
4 iload_2
5 bipush 10
7 if_icmpge 28 (+21)
10 iinc 1 by 1 //进入到for循环,将局部变量表中位置为1的变量进行自增+1的操作,即++i;此时i=1
13 iload_1 //将局部变量表1的位置压入操作数栈,即i,此时i=1
14 istore_1 //将操作数栈顶元素赋值到局部变量表1的位置即i=1, 可以看到此时是正常的自增操作,没有i=i++;的覆盖情况
15 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
18 iload_1
19 invokevirtual #3 <java/io/PrintStream.println : (I)V> //可以看到此时每次循环就会打印出自增的值
22 iinc 2 by 1
25 goto 4 (-21)
28 return
4. 总结
通过查看字节码指令,我们发现了i=i++; 和i=++i;的不同,i=i++;是先取i值(iload_1),然后自增(iinc 1 by 1),最后赋值(istore_1), 相当于先复制了一份i的副本,然后对原值自增,然后又把副本赋值给原值,这导致了自增一直被覆盖。而i=++i; 是先自增(iinc 1 by 1),然后取i值(iload_1),最后赋值(istore_1);这相当于先对原值自增,然后复制了一份副本, 最后把副本赋值给原值. 这就相当于每次拿的的副本都是最新的数据,所以每次自增都是正常。
5. 结尾
看了上面的解析,相信对内部的字节码指令执行已经有了清晰的了解,那么下面的代码允许结果是多少呢
@Test
void testIPP() {
int i = 0;
for (int j = 0; j < 10; j++) {
i++;
}
System.out.println(i);
} @Test
void testPPI() {
int i = 0;
for (int j = 0; j < 10; j++) {
++i;
}
System.out.println(i);
}
没错,都是10,这时for循环的内部内容其实都一样了,都是iinc 1 by 1了。
从jvm字节码指令看i=i++和i=++i的区别的更多相关文章
- JVM 字节码指令手册 - 查看 Java 字节码
JVM 字节码指令手册 - 查看 Java 字节码 jdk 进行的编译生成的 .class 是 16 进制数据文件,不利于学习分析.通过下命令 javap -c Demo.class > Dem ...
- 从字节码指令看重写在JVM中的实现
Java是解释执行的.包含动态链接的特性.都给解析或执行期间提供了非常多灵活扩展的空间.面向对象语言的继承.封装和多态的特性,在JVM中是怎样进行编译.解析,以及通过字节码指令怎样确定方法调用的版本号 ...
- 从JVM字节码执行看重载和重写
Java 重写(Override)与重载(Overload) 重写(Override) 重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变.即外壳不变,核心重写! 重写的 ...
- JVM字节码指令
invokevirtual 调用实例方法 invokespecial 调用父类构造,实例初始化方法,私有方法 dup 复制栈顶数值,并且复制值进栈,pop/pop2为栈顶值出栈 aload_0 加载第 ...
- [三] java虚拟机 JVM字节码 指令集 bytecode 操作码 指令分类用法 助记符
说明,本文的目的在于从宏观逻辑上介绍清楚绝大多数的字节码指令的含义以及分类 只要认真阅读本文必然能够对字节码指令集有所了解 如果需要了解清楚每一个指令的具体详尽用法,请参阅虚拟机规范 指令简介 计算机 ...
- [四] java虚拟机JVM编译器编译代码简介 字节码指令实例 代码到底编译成了什么形式
前言简介 前文已经对虚拟机进行过了简单的介绍,并且也对class文件结构,以及字节码指令进行了详尽的说明 想要了解JVM的运行机制,以及如何优化你的代码,你还需要了解一下,java编译器到底是 ...
- JVM学习第三天(JVM的执行子系统)之字节码指令
早上看了Class类文件结构,晚上继续来看字节码指令,毕竟谁也不是一步登天的(说白了还是穷); 字节码指令 Java虚拟机的指令由一个字节长度的.代表着某种特定操作含义的数字(称为操作码,Opcode ...
- JVM字节码之整型入栈指令(iconst、bipush、sipush、ldc)
官网:http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html 原文地址:http://www.linmuxi.com/2016/02 ...
- JVM总括三-字节码、字节码指令、JIT编译执行
JVM总括三-字节码.字节码指令.JIT编译执行 目录:JVM总括:目录 java文件编译后的class文件,java跨平台的中间层,JVM通过对字节码的解释执行(执行模式,还有JIT编译执行,下面讲 ...
随机推荐
- hdu 1503 Advanced Fruits(DP)
题意: 将两个英文单词进行合并.[最长公共子串只要保留一份] 输出合并后的英文单词. 思路: 求最长公共子串. 记录路径: mark[i][j]=-1:从mark[i-1][j]转移而来. mark[ ...
- POJ 1274 The Perfect Stall(二分图最大匹配)
题意: N头牛M个牛棚,每只牛都有它自己指定的若干个它愿意呆的牛棚. 每个牛棚最多呆一头牛. 问最多可以满足多少头牛的愿望. 思路: 裸二分图最大匹配. 代码: int n,m; vector< ...
- hdu 4788 Hard Disk Drive (水题)
题意: Input The first line contains an integer T, which indicates the number of test cases. For each t ...
- 一次fork引发的惨案!
"你还有什么要说的吗?没有的话我就要动手了",kill程序最后问道. 这一次,我没有再回答. 只见kill老哥手起刀落,我短暂的一生就这样结束了··· 我是一个网络程序,一直以来都 ...
- 前端---梳理 http 知识体系 1
最近看了http相关的知识点,觉得还是有必要整理下,这样对自己的网络知识体系也有帮助. http 是什么 http叫超文本传输协议,可以拆成超文本.传输.协议来理解 协议 http 是一个用在计算机里 ...
- Linux oracle 导入sql文件
1.@sql文件的路径 SQL>@/data/xx.sql; 2.导入完毕 commit;
- Jetbrains 系 IDE 编辑器的代码提示功能
著名的 Jetbrains 可谓编程界的一大福音,众多有名代码编辑器比如 ItelliJ IDEA.PHPStorm.WebStorm.PyCharm 等,均出自这家公司麾下. 对于中国的Java开发 ...
- svn与git区别
代码扫描工具介绍:https://baijiahao.baidu.com/s?id=1629218655164599200&wfr=spider&for=pc Git和SVN的区别与联 ...
- Git - git push origin master 报错的解决方法
亲测实用,转载保存,原文地址:https://blog.csdn.net/kangvcar/article/details/72773904 错误提示如下: [root@linux1 php]# gi ...
- DockerFile-构建容器的基石
DockerFile 非常的关键,它不同于 docker commit 的手动命令方式来进行镜像的构建和修改,类似 docker commit 的交互被称为命令式交互.命令式交互是运维一直绕不开的一种 ...