Skip to content

字节码指令集概述/分类/数据类型

字节码指令:JVM 的「机器语言」

前面我们学了 Class 文件的结构,知道字节码存储在 Code 属性中。这一节,我们来解码这些字节码到底在「说」什么。

字节码指令是 JVM 能够执行的最小操作单元。每一条指令都是一字节的操作码加上零或多个操作数。就像 CPU 的指令集,只不过 JVM 是软件模拟的 CPU。

指令的编码规则

单字节操作码

JVM 的操作码占 1 个字节(0~255),这意味着最多可以有 256 种指令。实际上 JVM 规范定义了约 200 种指令。

┌────────┐
│ opcode │  ← 1 字节,0~255
└────────┘

指令格式

[opcode] [operand1] [operand2] ...

比如:
  0x10 0x05    → bipush 5(把 byte 5 入栈)
  0xB1         → return(返回 void,无操作数)

指令分类一览

类别数量作用
加载存储指令~40在局部变量表和操作数栈之间移动数据
算术指令~30加减乘除、位运算
类型转换指令~20宽化、窄化类型转换
对象/数组指令~30创建对象、访问字段、数组操作
方法调用指令5invokevirtual、invokespecial、invokestatic、invokeinterface、invokedynamic
返回指令6ireturn、lreturn、areturn、return 等
栈操作指令~10dup、swap、pop 等
控制流指令~30if-、goto、tableswitch、lookupswitch
异常指令1athrow
同步指令2monitorenter、monitorexit

字节码的数据类型

JVM 指令集基于操作数的类型区分指令。虽然是「无类型虚拟机」,但许多指令会区分数据类型。

JVM 支持的数据类型

类型说明示例指令前缀
int32 位整数iadd、iload、iconst_0
long64 位长整数ladd、lload、lconst_0
float32 位浮点数fadd、fload、fconst_0
double64 位浮点数dadd、dload、dconst_0
reference对象引用areturn、aload、aconst_null
byte / short字节/短整型bipush、sipush
char字符bipush(用 int 表示)
boolean布尔iconst_0/1(用 int 表示)

类型后缀规则

大多数指令通过类型后缀区分操作数类型:

iload   // i = int
lload   // l = long
fload   // f = float
dload   // d = double
aload   // a = reference (对象/数组引用)

注意:bipushsipush 中,bs 不是 boolean 和 short,而是 byteshort

没有类型后缀的指令

有些指令不区分类型,或者说类型由上下文决定:

pop        // 弹出栈顶值,无类型概念
swap       // 交换栈顶两个值
goto       // 无条件跳转
tableswitch// switch 跳转表

指令的寻址方式

立即数寻址

操作数直接包含在指令中:

bipush 100   // 把 100 作为一个 byte 入栈
sipush 1000  // 把 1000 作为一个 short 入栈
iconst_5     // 把 int 5 入栈(隐含立即数)

局部变量寻址

通过局部变量表槽号访问:

iload 1     // 把局部变量槽 1 的 int 入栈
aload 2     // 把局部变量槽 2 的引用入栈
istore 3    // 把栈顶 int 存入槽 3

常量池寻址

通过常量池索引加载常量:

ldc #3        // 从常量池加载常量 #3
getfield #2   // 访问常量池 #2 指定的字段
invokevirtual #4  // 调用常量池 #4 指定的方法

常见指令速查

局部变量加载(iload 系列)

iload          // 从槽 n 加载 int(wide 指令)
iload_0        // 从槽 0 加载 int
iload_1        // 从槽 1 加载 int
iload_2        // 从槽 2 加载 int
iload_3        // 从槽 3 加载 int

iload_0 ~ iload_3iload 0 ~ iload 3 的高效压缩形式,opcode 分别是 0x26~0x29。

常量入栈(iconst 系列)

aconst_null    // null 引用入栈
iconst_m1      // -1 入栈
iconst_0       // 0 入栈
iconst_1       // 1 入栈
iconst_2       // 2 入栈
iconst_3       // 3 入栈
iconst_4       // 4 入栈
iconst_5       // 5 入栈
lconst_0       // 0L 入栈
lconst_1       // 1L 入栈
fconst_0       // 0.0f 入栈
fconst_1       // 1.0f 入栈
fconst_2       // 2.0f 入栈
dconst_0       // 0.0 入栈
dconst_1       // 1.0 入栈

bipush 和 sipush

bipush 127     // 把 signed byte 入栈(-128~127)
sipush 32767   // 把 signed short 入栈(-32768~32767)

查看方法的字节码

bash
# 先写一个简单的类
cat > Demo.java << 'EOF'
public class Demo {
    public static void main(String[] args) {
        int x = 10;
        int y = 20;
        int z = x + y;
    }
}
EOF

javac Demo.java
javap -c Demo.class

# 输出:
#   public static void main(java.lang.String[]);
#     Code:
#        0: bipush          10      // 把 10 入栈
#        2: istore_1                // 存到局部变量槽 1
#        3: bipush          20      // 把 20 入栈
#        5: istore_2                // 存到局部变量槽 2
#        6: iload_1                 // 槽 1 入栈(x)
#        7: iload_2                 // 槽 2 入栈(y)
#        8: iadd                     // 弹出两个 int,相加
#        9: istore_3                // 存到局部变量槽 3(z)
#       10: return                   // void 返回

操作数栈的直观理解

JVM 是基于栈的虚拟机,所有运算都在操作数栈上进行:

java
int result = 1 + 2;

对应的字节码执行过程:

初始状态:[empty]

执行 bipush 1:     [1]
执行 istore_0:    [empty]    (1 存入 slot 0)
执行 bipush 2:     [2]
执行 iadd:        [3]        (弹出 2 和 1,相加,结果 3 入栈)
执行 istore_1:   [empty]    (3 存入 slot 1)

宽化指令(wide)

当局部变量索引超过 3(不能用 iload_0~3),或者需要访问超出 255 个槽时,使用 wide 指令:

wide iload 255      // 加载槽 255 的 int(普通 iload 只能到槽 3)
wide iinc 1 by 100  // 槽 1 的 int 增加 100
wide 指令格式:
┌────────┬────────┬────────┐
│  0xC4  │ index  │ 可选操作数 │
└────────┴────────┴────────┘

指令集设计哲学

JVM 指令集的设计有几个核心理念:

1. 精简指令集

JVM 只有约 200 条指令,比真实 CPU 的指令集少得多。许多复杂操作需要多条简单指令组合。

java
// 源码
a = b + c;

// 字节码(4 条指令)
iload_1   // 加载 b
iload_2   // 加载 c
iadd      // 相加
istore_0  // 存入 a

2. 基于栈而非寄存器

简化了虚拟机的实现,移植性更好,但也意味着更多内存访问(数据要在栈和局部变量之间来回搬)。

3. 字节码不绑定特定 CPU

一次编译,到处执行。代价是需要 JVM 做翻译工作(解释执行或 JIT 编译)。

本节小结

字节码指令的核心要点:

维度说明
操作码1 字节,0~255,最多 256 种指令
数据类型int/long/float/double/reference 为主
寻址方式立即数、局部变量、常量池
指令分类加载存储、算术、类型转换、对象/数组、方法调用、返回、栈操作、控制流、异常、同步
设计哲学精简、基于栈、跨平台

下一节,我们来看 加载存储指令(局部变量/常量入栈)

基于 VitePress 构建