从字节码讲解i++和++i的区别|8月更文挑战

576 阅读6分钟

从字节码讲解i++和++i的区别

WangScaler: 一个用心创作的作者。

声明:才疏学浅,如有错误,恳请指正。

jvm栈

我们知道每一个线程有一个私有的虚拟机栈,在虚拟机栈中每个调用的方法又有一个栈帧,即方法的调用就是栈帧的入栈和出栈。

栈帧是一个内存区块,维系着方法执行的各种数据信息。不同的线程的栈帧不允许相互引用。

栈帧存储着局部变量表、操作数栈、动态链接、方法返回地址、一些附加信息。一些地方将动态链接、方法返回地址、一些附加信息并称为帧数据区。

  • 局部变量表:存储方法的参数、方法内的局部变量(基本数据类型和引用数据类型以及返回值类型的数据),32位的占一个变量槽,64位占两个变量槽。
  • 操作数栈:根据字节码指令,往栈中写入数据或提取数据。
  • 动态链接:指向运行时常量池,因为编译的时候,变量和方法都作为符号引用保存在常量池中,方法之间的调用就是通过这些符号引用来表示,而动态链接就是将这些符号引用,变成直接引用。
  • 方法返回地址:存放的调用该方法的PC寄存器的值,用户方法执行完之后回到被调用的位置。正常退出返回调用者的PC寄存器的值,即调用该方法的指令的下一条指令的地址。异常退出则根据异常表来确定返回的地址,栈帧不保存这部分信息。
  • 一些附加信息:java虚拟机的一些信息。

IDEA中查看类字节码

一、Idea Tools

使用jdk命令结合Idea Tools来查看。

添加工具

在File -> Settings -> Tools ->External Tools点击+号 image-20210730181319409.png

Name:随意填写
Program:$JDKPath$\bin\javap.exe
Arguments:-v -c $FileNameWithoutExtension$.class
Working directory:$OutputPath$$FileDirRelativeToSourcepath$

使用

当你打开java文件后,点击Tools->External Tools ->上述的Name值,即可看见字节码。

二、插件

安装插件

File ->Settings->Plugins搜索插件:jclasslib Bytecode Viewer 并安装

使用

使用之前先编译java文件,编译完之后,在右侧会有Jclasslib点开即可,如果没有,View->Show Bytecode With Jclasslib.

i++和++i的区别

示例一: i++

先通过这个简单的小例子入手,相信这个例子大家大会做。

package com.wangscaler.Increment;
​
/**
 * @author WangScaler
 * @date 2021/7/30 18:00
 */public class Main {
    public static void main(String[] args) {
        int i = 0;
        System.out.println(i++);
        System.out.println(i);
    }
}

打印的结果是0、1。我们通过字节码来看执行过程。

 0 iconst_0
 1 istore_1
 2 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
 5 iload_1
 6 iinc 1 by 1
 9 invokevirtual #3 <java/io/PrintStream.println : (I)V>
12 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
15 iload_1
16 invokevirtual #3 <java/io/PrintStream.println : (I)V>
19 return
  • i代表int,const代表常量,iconst_0就是将常量0入栈。

  • store代表存储,istore_1就是将栈顶的值出栈,放到局部变量1的位置。

    前两步示意图。

image-20210730184300279.png

  • 第三步动态链接运行时常量池#2,这句跟System.out.println()有关,这不过多介绍。
  • load就是加载变量的值,iload_1就是加载局部变量位置1的值到操作栈栈顶。

image-20210730192411568.png

  • inc就是自增操作,iinc 1 by 1 第一个1的意思是局部变量表位置1,所以整句话的意思就是在局部变量表1的值的基础上自增1。

image-20210730192423596.png

  • 调用打印的方法打印,打印的是栈中的值0
  • iload_1,同上将局部变量位置1的值(此时是1)入栈到栈顶
  • 打印1

示例二: ++i

在上述代码的基础上追加代码

System.out.println(++i);
System.out.println(i);

在实例一中i的值是1,所以现在的打印结果是2、2。还是通过字节码一探究竟。

19 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
22 iinc 1 by 1
25 iload_1
26 invokevirtual #3 <java/io/PrintStream.println : (I)V>
29 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
32 iload_1
33 invokevirtual #3 <java/io/PrintStream.println : (I)V>
36 return
  • 从第二句开始,iinc 1 by 1,局部变量表位置1的值自增1。所以现在局部变量表位置1的值为2
  • iload_1。将局部变量表1的值(2)入栈栈顶。所以操作数栈栈顶的值为2。
  • 调用打印,打印栈顶的值2。
  • 下面是重复操作,又将局部变量表的值重新入栈,但是期间值没有变化。

示例三: i= i++

继续追加代码

i = i++;
System.out.println(i);

此时i的初始值是2。打印的结果还是2。字节码如下:

6 iload_1
37 iinc 1 by 1
40 istore_1
41 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
44 iload_1
45 invokevirtual #3 <java/io/PrintStream.println : (I)V>
48 return
  • iload_1。再次将局部变量表1的值(2)入栈栈顶。所以现在局部变量表位置1的值为2。
  • iinc 1 by 1。局部变量表1的值自增1,当前值为3。
  • istore_1。将栈顶的值出栈,放入局部变量表1的位置。所以局部变量表的值又变成了2。
  • iload_1。将局部变量的值2入栈栈顶。
  • 调用打印,打印的值为2

从这可以看出,之前说的i++是先赋值再自增,其实是不对的,i++依然是先自增。但是对于值的计算 ,这个说法还是可以接受的。

示例四: i= ++i

继续追加代码

i = ++i;
System.out.println(i);

此时i的初始值是2,打印的结果是3

48 iinc 1 by 1
51 iload_1
52 istore_1
53 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
56 iload_1
57 invokevirtual #3 <java/io/PrintStream.println : (I)V>
60 return
  • iinc 1 by 1。局部变量表1的值自增1,自增之后的值是3。
  • iload_1。局部变量表1的值3入栈,当前栈顶的值3。
  • istore_1。栈顶元素出栈,放入局部变量1的位置。
  • 入栈栈顶元素值3
  • 调用打印,所以打印的值为3

示例五: int j= i++

追加代码如下:

int j = i++;
System.out.println(j);
System.out.println(i);

此时i的初始值为3。打印的结果你能算出来了吗?没错就是3、4。

字节码如下:

60 iload_1
61 iinc 1 by 1
64 istore_2
65 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
68 iload_2
69 invokevirtual #3 <java/io/PrintStream.println : (I)V>
72 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
75 iload_1
76 invokevirtual #3 <java/io/PrintStream.println : (I)V>
79 return
  • 局部变量表1的值3入栈,当前栈顶的值3。
  • iinc 1 by 1。局部变量表1的值自增1,当前值4。
  • istore_2。栈顶元素出栈,放入到局部变量表2的位置,注意不是位置1
  • iload_2。局部变量表2的值3入栈栈顶。
  • 调用打印语句,所以打印的是3。
  • iload_1。局部变量表1的值4入栈,当前栈顶的值4。
  • 调用打印语句,所以打印的是4。

示例六: j= ++i

追加代码如下,i的初始值4

j = ++i;
System.out.println(j);
System.out.println(i);

字节码如下:

79 iinc 1 by 1
82 iload_1
83 istore_2
84 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
87 iload_2
88 invokevirtual #3 <java/io/PrintStream.println : (I)V>
91 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
94 iload_1
95 invokevirtual #3 <java/io/PrintStream.println : (I)V>
98 return

示例七: i + i++ + ++i

继续追加代码。i的初始值是 5,

System.out.println(i + i++ + ++i);

字节码

 98 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
101 iload_1
102 iload_1
103 iinc 1 by 1
106 iadd
107 iinc 1 by 1
110 iload_1
111 iadd
112 invokevirtual #3 <java/io/PrintStream.println : (I)V>
115 return

牛刀一试

最后两个示例,我没有写结果,便于让你学以致用,自己去试试看看是否真的听懂了。