自增变量

本文主要记录自增变量 i++++1 的操作运算逻辑。

字节码分析

方法如下:

1
2
3
4
5
6
7
8
9
public 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
60
Compiled 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 方法指令解析:

指令行指令含义
0iconst_1把常量1压入操作数栈(iconst_<n>用于压入-1~5的int常量,效率更高)
1istore_1把操作数栈顶的1弹出,存入局部变量表Slot 1(即i = 1
2iload_1把Slot 1的i(值为1)压入操作数栈
3iinc 1, 1直接修改局部变量表Slot 1的i,自增1(i += 1,此时i=2);
⚠️ 注意:iinc不操作操作数栈,直接修改局部变量!
6istore_1把操作数栈顶的旧值(1)弹出,重新存入Slot 1(覆盖后i又变回1?
❗ 这里是关键:操作数栈里还是自增前的1,所以这行执行后i回到1)
7iload_1把Slot 1的i(值为1)压入操作数栈
8iinc 1, 1Slot 1的i自增1(i=2
11istore_2把操作数栈顶的旧值(1)弹出,存入Slot 2(即j = 1
12iload_1把Slot 1的i(值为2)压入操作数栈
13iinc 1, 1Slot 1的i自增1(i=3
16iload_1把Slot 1的i(值为3)压入操作数栈
17iload_1再次把Slot 1的i(值为3)压入操作数栈(此时栈顶:3,栈底:2)
18iinc 1, 1Slot 1的i自增1(i=4
21imul弹出操作数栈顶的两个3,执行乘法(3*3=9),结果压入栈
22iadd弹出栈顶的9和之前压入的2(第12行),执行加法(9+2=11),结果压入栈
23istore_3把栈顶的11弹出,存入Slot 3(即k = 11
24getstatic #7获取静态字段System.out(类型为PrintStream),压入操作数栈;#7指向常量池的java/lang/System.out:Ljava/io/PrintStream;
27iload_1把Slot 1的i(值为4)压入操作数栈
28invokedynamic #13, 0动态调用makeConcatWithConstants方法(JDK 9+的字符串拼接优化),把i拼接成字符串;#13是常量池索引,指向拼接逻辑
33invokevirtual #17调用PrintStream.println(String)方法,打印拼接后的i值;invokevirtual用于调用普通实例方法(动态分派)
36getstatic #7再次获取System.out,压入栈
39iload_2把Slot 2的j(值为1)压入栈
40invokedynamic #23, 0拼接j成字符串
45invokevirtual #17打印j的值
48getstatic #7第三次获取System.out,压入栈
51iload_3把Slot 3的k(值为11)压入栈
52invokedynamic #24, 0拼接k成字符串
57invokevirtual #17打印k的值
60returnmain方法执行完毕,返回(无返回值)

关键补充说明:

  1. iinc的“坑”iinc直接修改局部变量表,不影响操作数栈。比如第3行iinc 1,1i变成2,但操作数栈里还是第2行压入的旧值1,所以第6行istore_1会把i又改回1——这是字节码中“自增指令”和“栈操作”分离导致的典型现象。

  2. invokedynamic:这是JDK 7引入的动态调用指令,JDK 9+用于优化字符串拼接(替代传统的StringBuilder),makeConcatWithConstants是JVM提供的拼接方法,比手动拼接更高效。

  3. 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 = 4

  • j = 1

  • k = 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 = ? │
├───────────────────────────────────┤
│ 操作数栈(空) │
│ ┌───┐ │
│ │ │ │
│ └───┘ │
└───────────────────────────────────┘

这里画了一张图来更好地理解:

image2

步骤 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)时:

  1. getstatic方法区的运行时常量池找到 System.out 的引用,压入操作数栈;
  2. iload_1 压入 i=4,invokedynamic 拼接字符串,invokevirtual 调用 println;
  3. 每次 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」为局部变量自增指令(直接修改局部变量表,不操作操作数栈)。

❗核心重点:iinc指令是唯一直接操作局部变量表、不触碰操作数栈的指令,这也是“i=i++”后i值回退的根本原因(操作数栈保留旧值,最终覆盖局部变量表的自增后值)。

核心总结

  1. 局部变量表 是 “变量的仓库”,iload_<n> 取数、istore_<n> 存数;
  2. 操作数栈 是 “临时运算车间”,算术指令(imul/iadd)必须从栈中取数;
  3. iinc 是特例:直接修改局部变量表,不影响操作数栈,这是 i++ 字节码的核心坑点;
  4. 方法执行时,栈帧随方法调用创建,随方法结束销毁。

所以自增自减的关键点是 iinc

参考视频

JavaSE面试题:自增变量

0%
指令行字节码指令局部变量表状态(Slot: 变量名=值)操作数栈状态核心说明
初始状态main方法刚进入Slot0: args=null; Slot1: i=?; Slot2: j=?; Slot3: k=?[]栈帧创建完成,局部变量表仅args初始化(方法参数),其他局部变量未赋值,操作数栈为空
0iconst_1Slot0: args=null; Slot1: i=?; Slot2: j=?; Slot3: k=?[1]将int常量1压入操作数栈(iconst_用于压入-1~5的int常量)
1istore_1Slot0: args=null; Slot1: i=1; Slot2: j=?; Slot3: k=?[]弹出操作数栈顶的1,存入局部变量表Slot1,完成i的初始化(i=1)
2iload_1Slot0: args=null; Slot1: i=1; Slot2: j=?; Slot3: k=?[1]将局部变量表Slot1的i=1压入操作数栈
3iinc 1, 1Slot0: args=null; Slot1: i=2; Slot2: j=?; Slot3: k=?[1]直接修改局部变量表Slot1的i,自增1(i=2);此指令不操作操作数栈,栈顶仍为1
6istore_1Slot0: args=null; Slot1: i=1; Slot2: j=?; Slot3: k=?[]弹出操作数栈顶的旧值1,覆盖局部变量表Slot1的i,导致i从2回退为1
7iload_1Slot0: args=null; Slot1: i=1; Slot2: j=?; Slot3: k=?[1]将局部变量表Slot1的i=1压入操作数栈
8iinc 1, 1Slot0: args=null; Slot1: i=2; Slot2: j=?; Slot3: k=?[1]直接修改局部变量表Slot1的i,自增1(i=2);操作数栈仍保留旧值1
11istore_2Slot0: args=null; Slot1: i=2; Slot2: j=1; Slot3: k=?[]弹出操作数栈顶的旧值1,存入局部变量表Slot2,完成j的赋值(j=1)
12iload_1Slot0: args=null; Slot1: i=2; Slot2: j=1; Slot3: k=?[2]将局部变量表Slot1的i=2压入操作数栈
13iinc 1, 1Slot0: args=null; Slot1: i=3; Slot2: j=1; Slot3: k=?[2]直接修改局部变量表Slot1的i,自增1(i=3);操作数栈仍保留旧值2
16iload_1Slot0: args=null; Slot1: i=3; Slot2: j=1; Slot3: k=?[2, 3]将局部变量表Slot1的i=3压入操作数栈,栈内元素为[2(旧值), 3(当前i值)]
17iload_1Slot0: args=null; Slot1: i=3; Slot2: j=1; Slot3: k=?[2, 3, 3]再次将局部变量表Slot1的i=3压入操作数栈,为后续乘法运算准备两个因数
18iinc 1, 1Slot0: args=null; Slot1: i=4; Slot2: j=1; Slot3: k=?[2, 3, 3]直接修改局部变量表Slot1的i,自增1(i=4);操作数栈元素不变,仍为[2, 3, 3]
21imulSlot0: args=null; Slot1: i=4; Slot2: j=1; Slot3: k=?[2, 9]弹出操作数栈顶的两个3,执行int类型乘法(3×3=9),将结果9压入栈顶
22iaddSlot0: args=null; Slot1: i=4; Slot2: j=1; Slot3: k=?[11]弹出操作数栈顶的9和2,执行int类型加法(2+9=11)?修正:实际运算逻辑为“先压入的2(第12行) + 乘法结果9”,最终结果11压入栈顶(原反推源码k=12为笔误,此处以字节码实际运算为准)
23istore_3Slot0: args=null; Slot1: i=4; Slot2: j=1; Slot3: k=11[]弹出操作数栈顶的加法结果11,存入局部变量表Slot3,完成k的赋值(k=11)
24getstatic #7Slot0: args=null; Slot1: i=4; Slot2: j=1; Slot3: k=11[System.out]从方法区的运行时常量池获取静态字段System.out(PrintStream类型),将其引用压入操作数栈
27iload_1Slot0: args=null; Slot1: i=4; Slot2: j=1; Slot3: k=11[System.out, 4]将局部变量表Slot1的i=4压入操作数栈,为打印i做准备
28invokedynamic #13, 0Slot0: args=null; Slot1: i=4; Slot2: j=1; Slot3: k=11[System.out, “i=4”]动态调用makeConcatWithConstants方法(JDK9+字符串拼接优化),将i=4拼接为字符串,结果压入栈顶
33invokevirtual #17Slot0: args=null; Slot1: i=4; Slot2: j=1; Slot3: k=11[]调用PrintStream.println(String)方法,打印拼接后的i字符串;方法调用后操作数栈清空
36getstatic #7Slot0: args=null; Slot1: i=4; Slot2: j=1; Slot3: k=11[System.out]再次获取System.out引用,压入操作数栈,为打印j做准备
39iload_2Slot0: args=null; Slot1: i=4; Slot2: j=1; Slot3: k=11[System.out, 1]将局部变量表Slot2的j=1压入操作数栈
40invokedynamic #23, 0Slot0: args=null; Slot1: i=4; Slot2: j=1; Slot3: k=11[System.out, “j=1”]动态拼接j=1为字符串,结果压入栈顶
45invokevirtual #17Slot0: args=null; Slot1: i=4; Slot2: j=1; Slot3: k=11[]调用println方法打印j字符串,操作数栈清空
48getstatic #7Slot0: args=null; Slot1: i=4; Slot2: j=1; Slot3: k=11[System.out]第三次获取System.out引用,压入操作数栈,为打印k做准备
51iload_3Slot0: args=null; Slot1: i=4; Slot2: j=1; Slot3: k=11[System.out, 11]将局部变量表Slot3的k=11压入操作数栈
52invokedynamic #24, 0Slot0: args=null; Slot1: i=4; Slot2: j=1; Slot3: k=11[System.out, “k=11”]动态拼接k=11为字符串,结果压入栈顶
57invokevirtual #17Slot0: args=null; Slot1: i=4; Slot2: j=1; Slot3: k=11[]调用println方法打印k字符串,操作数栈清空
60returnSlot0: args=null; Slot1: i=4; Slot2: j=1; Slot3: k=11[]main方法执行完毕,无返回值;栈帧出栈销毁,程序结束