从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编译执行,下面讲 ...
随机推荐
- Centos7 误删除bin/sbin之类的恢复
参考连接:https://blog.csdn.net/weixin_41843733/article/details/107468767 挂载对应版本的光盘进入急救模式,复制已经丢失的命令到/mnt/ ...
- 深入浅出:了解时序数据库 InfluxDB
数据模型 1.时序数据的特征 时序数据应用场景就是在时间线上每个时间点都会从多个数据源涌入数据,按照连续时间的多种纬度产生大量数据,并按秒甚至毫秒计算的实时性写入存储. 传统的RDBMS数据库对写入的 ...
- Linux基本命令学习-文件基本操作1
关机重启 shutdown -h now #立即关机 shutdown -h 5 # 5秒后关机 #重启 shutdown -r now #立即重启 reboot halt #重启 文件相关 系统目录 ...
- OOP 4.21晚 指针知识点
1.读法:int* ptr ptr是一个指针指向整型变量 2.指针类型:指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型; 3.指针所指向的类型:只须把指针声明语句中的指针名字和名字左边的指 ...
- [Vue]浅谈Vue3组合式API带来的好处以及选项API的坏处
前言 如果是经验不够多的同志在学习Vue的时候,在最开始会接触到Vue传统的方式(选项式API),后边会接触到Vue3的新方式 -- 组合式API.相信会有不少同志会陷入迷茫,因为我第一次听到新的名词 ...
- oracle 与 前台 md5
创建函数: CREATE OR REPLACE FUNCTION MD5( passwd IN VARCHAR2) RETURN VARCHAR2 IS retval varchar2(32); BE ...
- python实现模糊操作
目录: (一)模糊或平滑与滤波的介绍 (二)均值模糊 (1) 原理 (2)代码实现-----均值模糊函数blur() (三)中值模糊------mediaBlur函数 (四)高斯模糊------Gau ...
- 算法题-n月后兔子数量
有一对兔子,从出生后第5个月起每个月都生一对兔子,小兔子长到第5个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少? public class test3 { public stati ...
- *(volatile unsigned int *)的理解
1. 解释 前面是无符号整型unsigned int的指针, 后面加一个地址,就是无符号整型的地址,前面又一个星号就是这个地址的值. 2.volatile 同步 因为同一个东西可能在不同的存储介质中有 ...
- android测试之monkey测试
1.首先安装SDK包 2.配置环境变量 3.打开CMD命令窗口,查看是否安装成功 命令:adb version 4.要做monkey测试的安卓包名,获取方式如下(必须启动要获取报名的app) 1.ad ...