自增变量
本文主要记录自增变量 i++ 和 ++1 的操作运算逻辑。
字节码分析
方法如下:1
2
3
4
5
6
7
8
9public static void main(String[] args) {
int i = 1;
i = i++;
int j = i++;
int k = i + ++i * i++;
System.out.println("i=" + i);
System.out.println("j=" + j);
System.out.println("k=" + k);
}
执行 javap -c -l xxx.class 命令:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60Compiled from "IncrementingVariable.java"
public class com.snails.interview.IncrementingVariable {
public com.snails.interview.IncrementingVariable();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/snails/interview/IncrementingVariable;
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: istore_1
2: iload_1
3: iinc 1, 1
6: istore_1
7: iload_1
8: iinc 1, 1
11: istore_2
12: iload_1
13: iinc 1, 1
16: iload_1
17: iload_1
18: iinc 1, 1
21: imul
22: iadd
23: istore_3
24: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
27: iload_1
28: invokedynamic #13, 0 // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String;
33: invokevirtual #17 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
36: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
39: iload_2
40: invokedynamic #23, 0 // InvokeDynamic #1:makeConcatWithConstants:(I)Ljava/lang/String;
45: invokevirtual #17 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
48: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
51: iload_3
52: invokedynamic #24, 0 // InvokeDynamic #2:makeConcatWithConstants:(I)Ljava/lang/String;
57: invokevirtual #17 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
60: return
LineNumberTable:
line 6: 0
line 7: 2
line 8: 7
line 9: 12
line 10: 24
line 11: 36
line 12: 48
line 13: 60
LocalVariableTable:
Start Length Slot Name Signature
0 61 0 args [Ljava/lang/String;
2 59 1 i I
12 49 2 j I
24 37 3 k I
}
main 方法指令解析:
| 指令行 | 指令 | 含义 |
|---|---|---|
| 0 | iconst_1 | 把常量1压入操作数栈(iconst_<n>用于压入-1~5的int常量,效率更高) |
| 1 | istore_1 | 把操作数栈顶的1弹出,存入局部变量表Slot 1(即i = 1) |
| 2 | iload_1 | 把Slot 1的i(值为1)压入操作数栈 |
| 3 | iinc 1, 1 | 直接修改局部变量表Slot 1的i,自增1(i += 1,此时i=2);⚠️ 注意: iinc不操作操作数栈,直接修改局部变量! |
| 6 | istore_1 | 把操作数栈顶的旧值(1)弹出,重新存入Slot 1(覆盖后i又变回1?❗ 这里是关键:操作数栈里还是自增前的 1,所以这行执行后i回到1) |
| 7 | iload_1 | 把Slot 1的i(值为1)压入操作数栈 |
| 8 | iinc 1, 1 | Slot 1的i自增1(i=2) |
| 11 | istore_2 | 把操作数栈顶的旧值(1)弹出,存入Slot 2(即j = 1) |
| 12 | iload_1 | 把Slot 1的i(值为2)压入操作数栈 |
| 13 | iinc 1, 1 | Slot 1的i自增1(i=3) |
| 16 | iload_1 | 把Slot 1的i(值为3)压入操作数栈 |
| 17 | iload_1 | 再次把Slot 1的i(值为3)压入操作数栈(此时栈顶:3,栈底:2) |
| 18 | iinc 1, 1 | Slot 1的i自增1(i=4) |
| 21 | imul | 弹出操作数栈顶的两个3,执行乘法(3*3=9),结果压入栈 |
| 22 | iadd | 弹出栈顶的9和之前压入的2(第12行),执行加法(9+2=11),结果压入栈 |
| 23 | istore_3 | 把栈顶的11弹出,存入Slot 3(即k = 11) |
| 24 | getstatic #7 | 获取静态字段System.out(类型为PrintStream),压入操作数栈;#7指向常量池的java/lang/System.out:Ljava/io/PrintStream; |
| 27 | iload_1 | 把Slot 1的i(值为4)压入操作数栈 |
| 28 | invokedynamic #13, 0 | 动态调用makeConcatWithConstants方法(JDK 9+的字符串拼接优化),把i拼接成字符串;#13是常量池索引,指向拼接逻辑 |
| 33 | invokevirtual #17 | 调用PrintStream.println(String)方法,打印拼接后的i值;invokevirtual用于调用普通实例方法(动态分派) |
| 36 | getstatic #7 | 再次获取System.out,压入栈 |
| 39 | iload_2 | 把Slot 2的j(值为1)压入栈 |
| 40 | invokedynamic #23, 0 | 拼接j成字符串 |
| 45 | invokevirtual #17 | 打印j的值 |
| 48 | getstatic #7 | 第三次获取System.out,压入栈 |
| 51 | iload_3 | 把Slot 3的k(值为11)压入栈 |
| 52 | invokedynamic #24, 0 | 拼接k成字符串 |
| 57 | invokevirtual #17 | 打印k的值 |
| 60 | return | main方法执行完毕,返回(无返回值) |
关键补充说明:
iinc的“坑”:
iinc直接修改局部变量表,不影响操作数栈。比如第3行iinc 1,1让i变成2,但操作数栈里还是第2行压入的旧值1,所以第6行istore_1会把i又改回1——这是字节码中“自增指令”和“栈操作”分离导致的典型现象。invokedynamic:这是JDK 7引入的动态调用指令,JDK 9+用于优化字符串拼接(替代传统的
StringBuilder),makeConcatWithConstants是JVM提供的拼接方法,比手动拼接更高效。LocalVariableTable:
Slot 0:
args(main方法参数,String数组),作用域0~61行;Slot 1:
i(int),作用域2~61行;Slot 2:
j(int),作用域12~61行;Slot 3:
k(int),作用域24~61行。
总结执行结果:
i = 4j = 1k = 11
JVM 运行时数据区变化
上面的分析可能相对于深奥,无法和虚拟机的运行时数据区映照起来,要理解字节码执行时 JVM 运行时数据区的变化,我们重点关注 局部变量表 和 操作数栈(这两个是方法执行的核心区域,属于虚拟机栈的栈帧)。
下面以 main 方法的关键指令为例,绘制栈帧状态变化图,并标注每一步对应的运行时数据区部分。
前置知识:运行时数据区核心组件
方法执行时,JVM 会为 main 创建一个栈帧,包含:
| 区域 | 作用 |
|---|---|
| 局部变量表(Slot) | 存储方法参数和局部变量,main 中 Slot 0=args、1=i、2=j、3=k |
| 操作数栈 | 临时存储运算数据,指令从栈中取数 / 存数,遵循 “后进先出”(FIFO) |
| 常量池引用 | 指向运行时常量池,解析 #7 #13 等常量索引(如 System.out) |
| 方法返回地址 | 记录方法执行完后回到哪里(main 结束后回到 JVM 启动器) |
关键指令的栈帧状态变化(以 main 为例)
我们选取 i 的初始化→i++→j 赋值→k 计算 的核心步骤,分步画图说明。
初始状态:main 方法刚进入
栈帧刚创建,局部变量表和操作数栈为空:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15【虚拟机栈 → main 栈帧】
┌───────────────────────────────────┐
│ 局部变量表(Slot) │
│ Slot 0: args = null(String[]) │
│ Slot 1: i = ? (未初始化) │
│ Slot 2: j = ? (未初始化) │
│ Slot 3: k = ? (未初始化) │
├───────────────────────────────────┤
│ 操作数栈(空) │
│ ┌───┐ │
│ │ │ 栈顶 │
│ └───┘ │
├───────────────────────────────────┤
│ 常量池引用 / 方法返回地址 │
└───────────────────────────────────┘
步骤 1:执行 0: iconst_1 + 1: istore_1(i = 1)
| 指令 | 作用 | 栈帧变化 |
|---|---|---|
0: iconst_1 | 把 int 常量 1 压入操作数栈 | 操作数栈:[1] |
1: istore_1 | 弹出栈顶 1,存入局部变量表 Slot 1 | 局部变量表 Slot 1: i = 1;操作数栈清空 |
栈帧状态:1
2
3
4
5
6
7
8
9
10
11
12
13【虚拟机栈 → main 栈帧】
┌───────────────────────────────────┐
│ 局部变量表(Slot) │
│ Slot 0: args = null │
│ Slot 1: i = 1 │ ← 存入 1
│ Slot 2: j = ? │
│ Slot 3: k = ? │
├───────────────────────────────────┤
│ 操作数栈(空) │
│ ┌───┐ │
│ │ │ │
│ └───┘ │ ← 弹出后清空
└───────────────────────────────────┘
步骤 2:执行 2: iload_1 + 3: iinc 1,1 + 6: istore_1(i = i++)
这是最关键的 “iinc 不操作栈” 的步骤:
| 指令 | 作用 | 栈帧变化 |
|---|---|---|
2: iload_1 | 把 局部变量表 Slot 1 的 i=1 压入操作数栈 | 操作数栈:[1] |
3: iinc 1,1 | 直接修改局部变量表 Slot 1,i 自增为 2(❗ 不碰操作数栈) | 局部变量表 Slot 1: i = 2;操作数栈仍为 [1] |
6: istore_1 | 弹出操作数栈顶的 旧值 1,覆盖 Slot 1 | 局部变量表 Slot 1: i = 1;操作数栈清空 |
栈帧状态(重点看 i 的回退):1
2
3
4
5
6
7
8
9
10
11
12
13【虚拟机栈 → main 栈帧】
┌───────────────────────────────────┐
│ 局部变量表(Slot) │
│ Slot 0: args = null │
│ Slot 1: i = 1 ← 被栈顶旧值覆盖 │
│ Slot 2: j = ? │
│ Slot 3: k = ? │
├───────────────────────────────────┤
│ 操作数栈(空) │
│ ┌───┐ │
│ │ │ │
│ └───┘ │
└───────────────────────────────────┘
❗ 核心坑点:iinc 改的是局部变量表,但 istore_1 存的是操作数栈的自增前旧值,所以 i 又变回 1。
步骤 3:执行 7: iload_1 + 8: iinc 1,1 + 11: istore_2(j = i++)
| 指令 | 作用 | 栈帧变化 |
|---|---|---|
7: iload_1 | 压入 Slot 1 的 i=1 → 操作数栈:[1] | 操作数栈:[1] |
8: iinc 1,1 | 局部变量表 Slot 1 → i=2(操作数栈仍为 [1]) | 局部变量表 Slot 1: i = 2;操作数栈仍为 [1] |
11: istore_2 | 弹出栈顶 1 → 存入 Slot 2 → j=1 | 局部变量表 Slot 1: i = 1;操作数栈清空 |
栈帧状态:1
2
3
4
5
6
7
8
9
10
11
12
13【虚拟机栈 → main 栈帧】
┌───────────────────────────────────┐
│ 局部变量表(Slot) │
│ Slot 0: args = null │
│ Slot 1: i = 2 ← iinc 后的值 │
│ Slot 2: j = 1 ← 存入栈顶旧值 │
│ Slot 3: k = ? │
├───────────────────────────────────┤
│ 操作数栈(空) │
│ ┌───┐ │
│ │ │ │
│ └───┘ │
└───────────────────────────────────┘
这里画了一张图来更好地理解:

步骤 4:执行 12: iload_1 + 13: iinc 1,1 + 16: iload_1 + 17: iload_1(k 计算前准备)
这是 k = i*i + i 的前置步骤,此时 i=2:
| 指令 | 作用 | 栈帧变化 |
|---|---|---|
12: iload_1 | 压入 i=2 → 操作数栈:[2] | 操作数栈:[2] |
13: iinc 1,1 | 局部变量表 i=3(操作数栈仍为 [2]) | 操作数栈:[2] |
16: iload_1 | 压入 i=3 → 操作数栈:[2, 3] | 操作数栈:[2, 3] |
17: iload_1 | 再压入 i=3 → 操作数栈:[2, 3, 3] | 操作数栈:[2, 3, 3] |
栈帧状态(操作数栈存满计算数据):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15【虚拟机栈 → main 栈帧】
┌───────────────────────────────────┐
│ 局部变量表(Slot) │
│ Slot 0: args = null │
│ Slot 1: i = 3 ← 第二次 iinc │
│ Slot 2: j = 1 │
│ Slot 3: k = ? │
├───────────────────────────────────┤
│ 操作数栈 │
│ ┌───┐ 栈顶 │
│ │ 3 │ ← 第17行压入 │
│ │ 3 │ ← 第16行压入 │
│ │ 2 │ ← 第12行压入 │
│ └───┘ │
└───────────────────────────────────┘
步骤 5:执行 18: iinc 1,1 + 21: imul + 22: iadd + 23: istore_3(k = 11)
| 指令 | 作用 | 栈帧变化 |
|---|---|---|
18: iinc 1,1 | 局部变量表 i=4(操作数栈不变,仍为 [2,3,3]) | 操作数栈:[2, 3, 3] |
21: imul | 弹出栈顶两个 3 → 3*3=9 → 压入栈 → 操作数栈:[2, 9] | 操作数栈:[2, 9] |
22: iadd | 弹出 2 和 9 → 2+9=11,iadd 后是 11, 结果被压入操作数栈 | 操作数栈:[11] |
23: istore_3 | 弹出计算结果 → 存入 Slot 3 → k=11 | 操作数栈:[] |
最终栈帧状态(k 赋值完成):1
2
3
4
5
6
7
8
9
10
11
12
13【虚拟机栈 → main 栈帧】
┌───────────────────────────────────┐
│ 局部变量表(Slot) │
│ Slot 0: args = null │
│ Slot 1: i = 4 ← 最终值 │
│ Slot 2: j = 1 │
│ Slot 3: k = 11 ← 计算结果 │
├───────────────────────────────────┤
│ 操作数栈(空) │
│ ┌───┐ │
│ │ │ │
│ └───┘ │
└───────────────────────────────────┘
后续打印指令的栈帧补充
当执行 24: getstatic #7(获取 System.out)时:
getstatic从方法区的运行时常量池找到System.out的引用,压入操作数栈;iload_1压入 i=4,invokedynamic拼接字符串,invokevirtual调用 println;- 每次 println 都会创建新的栈帧(属于
PrintStream.println方法),执行完后栈帧出栈。
整体运行时数据区关联图
把所有组件整合,就是完整的 JVM 运行时数据区:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18【JVM 运行时数据区】
┌───────────────────────────────────┐
│ 方法区 │
│ ├─ 运行时常量池(#7=System.out) │
│ ├─ 类信息(IncrementingVariable)│
│ └─ 静态变量 │
├───────────────────────────────────┤
│ 虚拟机栈 │
│ └─ main 栈帧(局部变量表+操作数栈)│
│ └─ println 栈帧(执行时创建) │
├───────────────────────────────────┤
│ 堆 │
│ ├─ System.out 对象(PrintStream)│
│ └─ 字符串常量(拼接后的字符串) │
├───────────────────────────────────┤
│ 程序计数器(PC 寄存器) │
│ 存储下一条执行的字节码指令地址 │
└───────────────────────────────────┘
main方法完整字节码指令的栈帧状态对照表
说明:
- 运行时数据区核心关注「虚拟机栈→main方法栈帧」中的「局部变量表」和「操作数栈」;
- 局部变量表Slot说明:Slot 0=args(String[],main方法参数)、Slot 1=i(int)、Slot 2=j(int)、Slot 3=k(int),未初始化时标注「?」;
- 操作数栈描述格式:「[栈底元素, …, 栈顶元素]」,空栈标注「[]」;
- 指令后缀「i」代表int类型,「iinc」为局部变量自增指令(直接修改局部变量表,不操作操作数栈)。