Skip to content

操作数栈/比较/跳转指令

栈操作指令

在深入控制流之前,先把栈操作指令搞清楚。这些指令用于直接操作操作数栈,不需要经过局部变量表。

栈顶操作:pop / pop2

pop     // 弹出栈顶一个槽的值(int/float/reference)
pop2    // 弹出栈顶两个槽的值(long/double,或两个 int)
java
// 源码
method(1, 2, 3);  // 假设 method 返回 void,但不需要结果

// 字节码
bipush        1
bipush        2
bipush        3
invokevirtual #method
pop                   // 弹出返回值(void 方法也会入栈再弹出)

pop vs pop2

栈: [value]       → pop    → [empty]       (int/float/reference)
栈: [v1, v2]     → pop2   → [empty]       (long/double)
栈: [v1, v2]     → pop2   → [empty]       (两个 int/float/reference)

交换:swap

swap    // 交换栈顶两个槽的值(类型必须相同或兼容)
java
// 源码:交换两个值
int a = x;
int b = y;
int temp = a;
a = b;
b = temp;

// 字节码(用 swap)
iload_1           // a 入栈
iload_2           // b 入栈
swap              // 交换:[b, a]
istore_1          // a = b
istore_2          // b = temp(a)

复制:dup 系列

这是最常用也最复杂的栈操作指令家族:

dup     // 复制栈顶值
dup_x1  // 复制栈顶值,插入到栈顶第二个值下面
dup_x2  // 复制栈顶值,插入到栈顶第三个值下面
dup2    // 复制栈顶两个值
dup2_x1 // 复制栈顶两个值,插入到栈顶第三个值下面
dup2_x2 // 复制栈顶两个值,插入到栈顶第四个值下面

dup 的典型用途:new + 构造函数

这是 dup 最常见的场景:

java
// 源码
User user = new User();

// 字节码
new #User            // 创建对象,引用入栈:[ref]
dup                  // 复制引用:[ref, ref]
invokespecial #init  // 调用构造器,弹出第一个 ref:[ref]
astore_1             // 存入 user:[empty]
执行过程:
new    → 栈: [ref1]
dup    → 栈: [ref1, ref2]   (ref1 和 ref2 是同一个对象的两个引用)
invokespecial → 栈: [ref1]  (弹出 ref2 用于构造器)
astore_1   → 栈: [empty]   (存入 ref1)

dup_x1 的用途

java
// 源码
long id = getId();    // 假设 getId() 返回 long
setId(id);           // 设置 id

// 字节码
invokevirtual #getId  // 返回 long 入栈:[l1, l2]
dup2                 // 复制两个槽:[l1, l2, l1, l2]
invokevirtual #setId // 调用 setId(long):弹出 [l1, l2],剩 [l1, l2]
lstore_1             // 存入 id:[empty]

栈操作指令速查

指令效果典型用途
pop弹出一个槽丢弃不需要的返回值
pop2弹出两个槽丢弃 long/double 返回值
swap交换栈顶两个槽交换两个值
dup复制栈顶一个槽new + 构造函数
dup_x1复制栈顶,插入第二位方法返回后处理
dup2复制栈顶两个槽处理 long/double 返回值

比较指令

int 比较:if 系列

ifeq  target      // 栈顶 == 0 时跳转
ifne  target      // 栈顶 != 0 时跳转
iflt  target      // 栈顶 < 0 时跳转
ifle  target      // 栈顶 <= 0 时跳转
ifgt  target      // 栈顶 > 0 时跳转
ifge  target      // 栈顶 >= 0 时跳转

注意:这些指令比较的是栈顶值与 0,不是两个栈顶值。

java
// 源码
if (x == 0) { doSomething(); }

// 字节码
iload_1            // x 入栈
ifeq 10            // x == 0 则跳到偏移 10
// 否则继续执行

int 比较两个值:if_icmp 系列

if_icmpeq target   // 栈顶 == 第二个栈顶值 时跳转
if_icmpne target   // 栈顶 != 第二个栈顶值 时跳转
if_icmplt target   // 栈顶 < 第二个栈顶值 时跳转
if_icmple target   // 栈顶 <= 第二个栈顶值 时跳转
if_icmpgt target   // 栈顶 > 第二个栈顶值 时跳转
if_icmpge target   // 栈顶 >= 第二个栈顶值 时跳转
java
// 源码
if (a > b) { return 1; } else { return 0; }

// 字节码
iload_1            // a 入栈:[a]
iload_2            // b 入栈:[a, b]
if_icmpgt 12       // a > b 则跳转:[a, b] 被弹出
iconst_0           // else: 0 入栈
ireturn            // 返回 0
iconst_1           // if: 1 入栈
ireturn            // 返回 1

执行过程图示:

iload_1:        [a]
iload_2:        [a, b]
if_icmpgt 12:   如果 a > b,跳到偏移 12 执行 iconst_1
                否则继续执行 iconst_0 + ireturn

reference 比较

if_acmpeq target    // 两个引用相等时跳转
if_acmpne target    // 两个引用不等时跳转
java
// 源码
if (obj1 == obj2) { ... }

// 字节码
aload_1            // obj1 入栈
aload_2            // obj2 入栈
if_acmpeq 20       // 相等则跳转

比较零值与比较两值的对比

指令比较的是什么操作数
ifeq栈顶值 == 0只需弹出一个值
if_icmpeq栈顶 == 第二个栈顶值需弹出两个值

跳转指令

无条件跳转:goto

goto target         // 无条件跳转到目标偏移
goto_w target       // 宽化跳转,支持更大的偏移范围
java
// 源码
while (true) {
    // 循环体
}
goto 0             // 字节码中的无条件跳转

条件跳转:if 系列

条件跳转和 if 系列比较指令配合使用:

ifeq        label1    // == 0
ifne        label1    // != 0
iflt        label1    // < 0
ifle        label1    // <= 0
ifgt        label1    // > 0
ifge        label1    // >= 0
if_icmpeq   label1    // == (两个 int)
if_icmpne   label1    // != (两个 int)
if_icmplt   label1    // <  (两个 int)
if_icmple   label1    // <= (两个 int)
if_icmpgt   label1    // >  (两个 int)
if_icmpge   label1    // >= (两个 int)

switch 指令:tableswitch 和 lookupswitch

tableswitch:稀疏跳转表

当 case 值密集时使用(如 switch 枚举的 int):

java
// 源码
switch (x) {
    case 0: return 0;
    case 1: return 10;
    case 2: return 20;
    case 3: return 30;
    default: return -1;
}

字节码使用 tableswitch

tableswitch 0 to 3
    0: 偏移 16
    1: 偏移 24
    2: 偏移 32
    3: 偏移 40
default: 偏移 48

特点:访问时间 O(1),但如果有大量空 case 会浪费空间。

lookupswitch:稀疏跳转表

当 case 值稀疏时使用:

java
// 源码
switch (c) {
    case 'A': return 1;   // ASCII 65
    case 'Z': return 26;  // ASCII 90
    case 'a': return 27;  // ASCII 97
    default: return 0;
}

字节码使用 lookupswitch

lookupswitch
    65:  偏移 16   (case 'A')
    90:  偏移 24   (case 'Z')
    97:  偏移 32   (case 'a')
default: 偏移 40

特点:需要线性搜索或哈希查找,但节省空间。

goto_w:宽化跳转

当跳转目标超出 16 位偏移范围时:

goto_w 100000    // 跳转到偏移 100000(超出 goto 的 16 位范围)

if-else 的字节码实现

简单 if

java
// 源码
if (x > 0) {
    result = x;
}
iload_1            // x 入栈
ifngt 8            // x > 0 则跳转
bipush        0    // x <= 0,result = 0
istore_2
iconst_1           // x > 0,跳到这里,result = x
istore_2

if-else

java
// 源码
if (x > 0) {
    result = x;
} else {
    result = -x;
}
iload_1            // x 入栈
ifgt 12            // x > 0 则跳到 12
iload_1            // else 分支:x 入栈
ineg               // -x
goto 15            // 跳过 if 分支
iconst_0           // (x <= 0,不执行)
nop                //
iload_1            // if 分支:x 入栈
istore_2           // result = x

三元表达式

java
// 源码
int max = (a > b) ? a : b;
iload_1            // a 入栈
iload_2            // b 入栈
if_icmpgt 14       // a > b 则跳转
iload_2            // else: b 入栈
goto 17            // 跳过 if 分支
iload_1            // if: a 入栈
istore_3           // 存入 max

三元表达式和 if-else 的字节码几乎一样

循环的字节码实现

while 循环

java
// 源码
int i = 0;
while (i < 10) {
    i++;
}

字节码:

iconst_0            // i = 0
istore_1
goto 10             // 跳到条件判断
iload_1             // 循环体开始
iconst_1
iadd
istore_1            // i++
iload_1             // 条件判断
bipush          10
if_icmplt 0         // i < 10 则跳回循环体(偏移 0)
// 循环结束

for 循环

java
// 源码
for (int i = 0; i < 10; i++) {
    System.out.println(i);
}

字节码结构:

初始化部分(循环外)
goto 条件判断
循环体
递增/递减部分
条件判断 → 循环体 or 跳出

do-while 循环

java
// 源码
int i = 0;
do {
    i++;
} while (i < 10);
iconst_0
istore_1             // i = 0
iload_1              // 循环体开始
iconst_1
iadd
istore_1             // i++
iload_1              // 条件判断
bipush          10
if_icmplt 0          // i < 10 则跳转回偏移 0

do-while 和 while 的区别:do-while 循环体至少执行一次,所以不需要在开头跳到条件判断。

完整示例

java
// 源码
public static int fibonacci(int n) {
    if (n <= 1) {
        return n;
    }
    int a = 0, b = 1;
    for (int i = 2; i <= n; i++) {
        int temp = a + b;
        a = b;
        b = temp;
    }
    return b;
}

字节码分析(关键部分):

bash
javap -c FibDemo.class

#  public static int fibonacci(int);
#    Code:
#       0: iload_0                // n 入栈
#       1: iconst_1              // 1 入栈
#       2: if_icmple 22           // n <= 1 则跳到偏移 22(return n)
#
#       5: iconst_0              // a = 0
#       6: istore_1
#       7: iconst_1              // b = 1
#       8: istore_2
#       9: iconst_2              // i = 2
#      10: istore_3
#      11: iload_3               // i 入栈
#      12: iload_0               // n 入栈
#      13: if_icmpgt 28           // i > n 则跳出循环
#
#      16: iload_1               // temp = a + b
#      17: iload_2
#      18: iadd
#      19: istore 4
#      21: iload_2               // a = b
#      22: istore_1
#      23: iload 4               // b = temp
#      25: istore_2
#      26: iinc 3 by 1           // i++
#      29: goto 11               // 跳回条件判断
#
#      32: iload_2               // return b
#      33: ireturn

本节小结

操作数栈/比较/跳转核心要点:

类别指令说明
栈操作pop/pop2/swap/dup直接操作栈
比较零值ifeq/ifne/iflt栈顶值与 0 比较
比较两个值if_icmpeq/if_icmpne两个栈顶值比较
引用比较if_acmpeq/if_acmpne两个引用比较
无条件跳转goto/goto_w无条件跳转到目标
switchtableswitch/lookupswitch密集用前者,稀疏用后者

理解栈操作和跳转指令的组合,就能分析任何 if/while/for/switch 的字节码实现。

下一节,我们来看 异常处理/同步控制指令

基于 VitePress 构建