对于 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)Iflags: (0x0001) ACC_PUBLICCode:stack=1, locals=3, args_size=20: iload_1 // 将局部变量表1号槽位的元素的值(即变量i的值)压入操作栈1: iinc 1, 1 // 局部变量表1号元素自增14: istore_2 // 将操作栈栈顶元素弹出,并将值赋值给局部变量表2号槽位元素(即变量a)5: iload_2 // 将局部变量表2号槽位元素的值压入操作栈6: ireturn // 返回操作栈栈顶元素LocalVariableTable:(局部变量表)Start Length Slot Name Signature0 7 0 this Ltest/Demo;0 7 1 i I5 2 2 a I
a = ++i
// 源码public int method(int i) {int a = ++i;return a;}
// 字节码public int method(int);descriptor: (I)Iflags: (0x0001) ACC_PUBLICCode:stack=1, locals=3, args_size=20: iinc 1, 1 // 局部变量表1号元素(即变量i)自增13: iload_1 // 将局部变量表1号槽位的元素的值压入操作栈4: istore_2 // 将操作栈栈顶元素弹出,并将值赋值给局部变量表2号槽位元素(即变量a)5: iload_2 // 将局部变量表2号槽位元素的值压入操作栈6: ireturn // 返回操作栈栈顶元素LocalVariableTable:(局部变量表)Start Length Slot Name Signature0 7 0 this Ltest/Demo;0 7 1 i I5 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: ()Iflags: (0x0001) ACC_PUBLICCode:stack=2, locals=3, args_size=10: 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号槽位自增114: istore_1 // 将操作栈栈顶元素弹出,并赋值给局部变量表1号槽位(对应的变量)15: iinc 2, 1 // 局部变量表1号槽位自增118: goto 4 // 跳转到第4行21: iload_1 // 将局部变量表1号槽位的值入栈22: ireturn // 返回栈顶元素LocalVariableTable:(局部变量表)Start Length Slot Name Signature4 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号槽位自增114: istore_1 // 将操作栈栈顶元素弹出,并赋值给局部变量表1号槽位(对应的变量)
下图模拟了这三条指令的执行过程。
该题也提醒我们 i++ 其实并非原子操作,即使对 i 使用 volatile 关键字修饰,如果多线程进行写操作,同样会产生数据互相覆盖的问题。
