对于 a = i++
与 a = ++i
的区别,一般回答是:一个是“先赋值再自增”,另一个是“先自增再赋值”,这个回答无可厚非,但不够深入,本文主要从字节码分析二者的不同。
JVM 内存布局
下图是一个经典的 JVM 内存分布图,本文仅对与本文内容有关的 JVM Stacks 进行简要介绍。
*本图参考了《码出高效 —— Java 开发手册》
JVM Stacks 是描述 Java 方法执行的内存区域,它是线程私有的。
栈中的元素用于支持虚拟机进行方法调用,每个方法从开始调用到被执行完成的过程,就是栈帧从入栈到出栈的过程。
栈帧是方法运行的基本结构,一个方法在被执行时,所有指令都只能针对当前栈帧进行操作。
栈帧
一个栈帧包括局部变量表、操作栈、动态连接、方法返回地址等,这里仅对局部变量表和操作栈进行简要介绍。
- 局部变量表是存放方法参数和局部变量的区域,最小的存储单元称为 Slot(槽);
- 操作栈是一个初始状态为空的桶式结构栈,在方法执行过程中,会有各种指令往栈中写入和提取信息。与局部变量表不同的是,操作栈对于数据的访问不是通过下标,而是通过标准的栈操作来进行的。
下面以 i++ 和 ++i 为例,说明操作栈与局部变量表的交互。
i++ 与 ++i
我们先对源代码进行编译,再用 javap 工具进行反编译。
a = i++
// 源码
public int method(int i) {
int a = i++;
return a;
}
// 字节码
public int method(int);
descriptor: (I)I
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=3, args_size=2
0: iload_1 // 将局部变量表1号槽位的元素的值(即变量i的值)压入操作栈
1: iinc 1, 1 // 局部变量表1号元素自增1
4: istore_2 // 将操作栈栈顶元素弹出,并将值赋值给局部变量表2号槽位元素(即变量a)
5: iload_2 // 将局部变量表2号槽位元素的值压入操作栈
6: ireturn // 返回操作栈栈顶元素
LocalVariableTable:(局部变量表)
Start Length Slot Name Signature
0 7 0 this Ltest/Demo;
0 7 1 i I
5 2 2 a I
a = ++i
// 源码
public int method(int i) {
int a = ++i;
return a;
}
// 字节码
public int method(int);
descriptor: (I)I
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=3, args_size=2
0: iinc 1, 1 // 局部变量表1号元素(即变量i)自增1
3: iload_1 // 将局部变量表1号槽位的元素的值压入操作栈
4: istore_2 // 将操作栈栈顶元素弹出,并将值赋值给局部变量表2号槽位元素(即变量a)
5: iload_2 // 将局部变量表2号槽位元素的值压入操作栈
6: ireturn // 返回操作栈栈顶元素
LocalVariableTable:(局部变量表)
Start Length Slot Name Signature
0 7 0 this Ltest/Demo;
0 7 1 i I
5 2 2 a I
我们对比两份字节码,不同之处如下所示:
a = i++ | a = ++i |
---|---|
0: iload_1 1: iinc 1, 1 4: istore_2 |
0: iinc 1, 1 3: iload_1 4: istore_2 |
综上所述,从字节码分析来看,i++ 是先将变量 i 的值入栈,再对局部变量表中的 i 进行自增;++i 是先对局部变量表中的 i 进行自增,再将 i 的值压入操作栈。
上面的结论有意的指出
iload_1
命令是元素的值入栈,而不是元素入栈,这是为了避免使用“元素入栈”而让人觉得iinc 1, 1
命令会使已经入栈的值自增 1,其实iinc
命令仅操作局部变量表中的元素,对操作栈无影响。
一道面试题
// 问题:该函数的返回值是多少?
public int method() {
int count = 0;
for (int i = 0; i < 10; i++) {
count = count++;
}
return count; // return 0
}
上述代码的返回值为 0,我们先对源代码进行编译,再反编译为字节码。
public int method();
descriptor: ()I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: iconst_0 // 将常量0入入栈
1: istore_1 // 将栈顶元素弹出,并赋值给局部变量表1号槽位(对应的变量)
2: iconst_0 // 将常量0入入栈
3: istore_2 // 将操作栈栈顶元素弹出,并赋值给局部变量表2号槽位(对应的变量)
4: iload_2 // 将局部变量表2号槽位的值入栈
5: bipush 100 // 将常量100入栈
7: if_icmpge 21 // 如果栈顶元素的值大于等于其后一个元素的值,则跳至第21行
10: iload_1 // 将局部变量表1号槽位的值入栈
11: iinc 1, 1 // 局部变量表1号槽位自增1
14: istore_1 // 将操作栈栈顶元素弹出,并赋值给局部变量表1号槽位(对应的变量)
15: iinc 2, 1 // 局部变量表1号槽位自增1
18: goto 4 // 跳转到第4行
21: iload_1 // 将局部变量表1号槽位的值入栈
22: ireturn // 返回栈顶元素
LocalVariableTable:(局部变量表)
Start Length Slot Name Signature
4 17 2 i I // 2号槽位
0 23 0 this Ltest/Demo;
2 21 1 count I // 1号槽位
使得返回值为 0 的关键在于下面 3 行字节码:
10: iload_1 // 将局部变量表1号槽位的值入栈
11: iinc 1, 1 // 局部变量表1号槽位自增1
14: istore_1 // 将操作栈栈顶元素弹出,并赋值给局部变量表1号槽位(对应的变量)
下图模拟了这三条指令的执行过程。
该题也提醒我们 i++
其实并非原子操作,即使对 i
使用 volatile 关键字修饰,如果多线程进行写操作,同样会产生数据互相覆盖的问题。