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. CentOS部署多台服务器JDK(shell脚本部署)

    部署7台新服务器的jdk,数量不算多,但也不打算一台一台的部署,写了个脚本执行 [ #!/bin/bash# JDK 安装包名jdk_packge="jdk-8u162-linux-x64. ...

  2. Apache Solr应用服务器存在远程代码执行漏洞👻

    Apache Solr应用服务器存在远程代码执行漏洞 1.描述 Apache Solr是一个开源的搜索服务,使用Java语言开发,主要基于HTTP和Apache Lucene实现的. Solr是一个高 ...

  3. k8s入坑之路(16)kubernetes中CICD/基于宿主机jenkins

    cicd的结合组件 需要代码仓库如gitlab.github.包构建工具Maven等,持续集成工具如jenkins,github/cicd.结合自己脚本实现重复式任务自动化. 传统服务发布流程: 提交 ...

  4. SpringBoot 全局异常拦截捕获处理

    一.全局异常处理 //Result定义全局数据返回对象 package com.xiaobing.demo001.domain; public class Result { private Integ ...

  5. JMeter学习笔记--工具简单介绍

    一.JMeter 介绍 Apache JMeter是纯JAVA桌面应用程序,被设计为用于测试客户端/服务端结构的软件(例如web应用程序).它可以用来测试静态和动态资源的性能,例如:静态文件,Java ...

  6. 基于大量图片与实例深度解析Netty中的核心组件

    本篇文章主要详细分析Netty中的核心组件. 启动器Bootstrap和ServerBootstrap作为Netty构建客户端和服务端的路口,是编写Netty网络程序的第一步.它可以让我们把Netty ...

  7. Visual Studio 2019连接MySQL数据库详细教程

    前言 如果要在 Visual Studio 2019中使用MySQL数据库,首先需要下载MySQL的驱动 Visual Studio默认只显示微软自己的SQL Server数据源,点击其它也是微软自己 ...

  8. 【CVE-2020-1948】Apache Dubbo Provider反序列化漏洞复现

    一.实验简介 实验所属系列: 系统安全 实验对象:本科/专科信息安全专业 相关课程及专业: 计算机网络 实验时数(学分):2 学时 实验类别: 实践实验类 二.实验目的 Apache Dubbo是一款 ...

  9. airflow redis sentinel

    获取master name subscribe __sentinel__:hello mysql plugin table not found mysqld --initialize-insecure ...

  10. layui表格-template模板的三种用法

    问题情境: layui中将数据库数据通过layui table渲染到前端表格,非常简单,但是如果数据库存储的信息不能被直接展示,项目中该页面有好几个这样的字段,会员类型,支付类型,会员时长还有平台类型 ...