从字节码讲解i++和++i的区别
WangScaler: 一个用心创作的作者。声明:才疏学浅,如有错误,恳请指正。
jvm栈
我们知道每一个线程有一个私有的虚拟机栈,在虚拟机栈中每个调用的方法又有一个栈帧,即方法的调用就是栈帧的入栈和出栈。
栈帧是一个内存区块,维系着方法执行的各种数据信息。不同的线程的栈帧不允许相互引用。
栈帧存储着局部变量表、操作数栈、动态链接、方法返回地址、一些附加信息。一些地方将动态链接、方法返回地址、一些附加信息并称为帧数据区。
- 局部变量表:存储方法的参数、方法内的局部变量(基本数据类型和引用数据类型以及返回值类型的数据),32位的占一个变量槽,64位占两个变量槽。
- 操作数栈:根据字节码指令,往栈中写入数据或提取数据。
- 动态链接:指向运行时常量池,因为编译的时候,变量和方法都作为符号引用保存在常量池中,方法之间的调用就是通过这些符号引用来表示,而动态链接就是将这些符号引用,变成直接引用。
- 方法返回地址:存放的调用该方法的PC寄存器的值,用户方法执行完之后回到被调用的位置。正常退出返回调用者的PC寄存器的值,即调用该方法的指令的下一条指令的地址。异常退出则根据异常表来确定返回的地址,栈帧不保存这部分信息。
- 一些附加信息:java虚拟机的一些信息。
IDEA中查看类字节码
一、Idea Tools
使用jdk命令结合Idea Tools来查看。
添加工具
在File -> Settings -> Tools ->External Tools点击+号
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的位置。
前两步示意图。
- 第三步动态链接运行时常量池#2,这句跟System.out.println()有关,这不过多介绍。
- load就是加载变量的值,iload_1就是加载局部变量位置1的值到操作栈栈顶。
- inc就是自增操作,iinc 1 by 1 第一个1的意思是局部变量表位置1,所以整句话的意思就是在局部变量表1的值的基础上自增1。
- 调用打印的方法打印,打印的是栈中的值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
牛刀一试
最后两个示例,我没有写结果,便于让你学以致用,自己去试试看看是否真的听懂了。