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的区别的更多相关文章

  1. JVM 字节码指令手册 - 查看 Java 字节码

    JVM 字节码指令手册 - 查看 Java 字节码 jdk 进行的编译生成的 .class 是 16 进制数据文件,不利于学习分析.通过下命令 javap -c Demo.class > Dem ...

  2. 从字节码指令看重写在JVM中的实现

    Java是解释执行的.包含动态链接的特性.都给解析或执行期间提供了非常多灵活扩展的空间.面向对象语言的继承.封装和多态的特性,在JVM中是怎样进行编译.解析,以及通过字节码指令怎样确定方法调用的版本号 ...

  3. 从JVM字节码执行看重载和重写

    Java 重写(Override)与重载(Overload) 重写(Override) 重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变.即外壳不变,核心重写! 重写的 ...

  4. JVM字节码指令

    invokevirtual 调用实例方法 invokespecial 调用父类构造,实例初始化方法,私有方法 dup 复制栈顶数值,并且复制值进栈,pop/pop2为栈顶值出栈 aload_0 加载第 ...

  5. [三] java虚拟机 JVM字节码 指令集 bytecode 操作码 指令分类用法 助记符

    说明,本文的目的在于从宏观逻辑上介绍清楚绝大多数的字节码指令的含义以及分类 只要认真阅读本文必然能够对字节码指令集有所了解 如果需要了解清楚每一个指令的具体详尽用法,请参阅虚拟机规范 指令简介 计算机 ...

  6. [四] java虚拟机JVM编译器编译代码简介 字节码指令实例 代码到底编译成了什么形式

      前言简介   前文已经对虚拟机进行过了简单的介绍,并且也对class文件结构,以及字节码指令进行了详尽的说明 想要了解JVM的运行机制,以及如何优化你的代码,你还需要了解一下,java编译器到底是 ...

  7. JVM学习第三天(JVM的执行子系统)之字节码指令

    早上看了Class类文件结构,晚上继续来看字节码指令,毕竟谁也不是一步登天的(说白了还是穷); 字节码指令 Java虚拟机的指令由一个字节长度的.代表着某种特定操作含义的数字(称为操作码,Opcode ...

  8. JVM字节码之整型入栈指令(iconst、bipush、sipush、ldc)

    官网:http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html 原文地址:http://www.linmuxi.com/2016/02 ...

  9. JVM总括三-字节码、字节码指令、JIT编译执行

    JVM总括三-字节码.字节码指令.JIT编译执行 目录:JVM总括:目录 java文件编译后的class文件,java跨平台的中间层,JVM通过对字节码的解释执行(执行模式,还有JIT编译执行,下面讲 ...

随机推荐

  1. html5的Message信息提示框

    1.定义dialog <dialog id="showDialog"> <div>您没有权限访问本页面!</div> </dialog&g ...

  2. Java8新特性之Optional,如何优雅地处理空指针

    是什么 ​ 从 Java 8 引入的一个很有趣的特性是 Optional 类.Optional 类主要解决的问题是臭名昭著的空指针异常(NullPointerException)-- 每个 Java ...

  3. flask cache

    http://brunorocha.org/python/flask/using-flask-cache.html 如何在大项目中使用cache 新建全局cache.py cache = Cache( ...

  4. 微信小程序(五)

    JavaScript: JavaScript 是一种轻量的,解释型的,面对对象的头等函数语言,是一种动态的基于原型和多范式的脚本语言,支持面向对象,命令式和函数式的编程风格. Nodejs 中的Jav ...

  5. linux 系统ssh超时设置

    1.修改client端的etc/ssh/ssh_config添加以下:(在没有权限改server配置的情形下) ServerAliveInterval 60 #client每隔60秒发送一次请求给se ...

  6. win10的pycharm中安装ansible模块过程

    前面的安装报错信息 ansible模块安装报错:Could not install packages due to an OSError: [Errno 2] No such file or dire ...

  7. printf("%d\n",printf("%d",printf("%d",i)));

    #include <stdio.h> int printf( const char *format, ... );首先 得看printf的返回类型是 int 这个函数的返回值是 你输出的位 ...

  8. 理解ASP.NET Core - 错误处理(Handle Errors)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或[点击此处查看全文目录](https://www.cnblogs.com/xiaoxiaotank/p/151852 ...

  9. [hdu6990]Directed Minimum Spanning Tree

    模板题:在有向图中,对每一个点求以其为根的最小(外向)生成树 (当图是强连通时)可以使用朱刘算法,算法过程如下: 1.对每一个节点,选择指向该点的边权最小的边,即得到一张子图 2.任选这张子图的一个简 ...

  10. SpringCloud微服务实战——搭建企业级开发框架(二十二):基于MybatisPlus插件TenantLineInnerInterceptor实现多租户功能

    多租户技术的基本概念:   多租户技术(英语:multi-tenancy technology)或称多重租赁技术,是一种软件架构技术,它是在探讨与实现如何于多用户的环境下共用相同的系统或程序组件,并且 ...