字节码指令集概述/分类/数据类型
字节码指令: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 | 创建对象、访问字段、数组操作 |
| 方法调用指令 | 5 | invokevirtual、invokespecial、invokestatic、invokeinterface、invokedynamic |
| 返回指令 | 6 | ireturn、lreturn、areturn、return 等 |
| 栈操作指令 | ~10 | dup、swap、pop 等 |
| 控制流指令 | ~30 | if-、goto、tableswitch、lookupswitch |
| 异常指令 | 1 | athrow |
| 同步指令 | 2 | monitorenter、monitorexit |
字节码的数据类型
JVM 指令集基于操作数的类型区分指令。虽然是「无类型虚拟机」,但许多指令会区分数据类型。
JVM 支持的数据类型
| 类型 | 说明 | 示例指令前缀 |
|---|---|---|
int | 32 位整数 | iadd、iload、iconst_0 |
long | 64 位长整数 | ladd、lload、lconst_0 |
float | 32 位浮点数 | fadd、fload、fconst_0 |
double | 64 位浮点数 | 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 (对象/数组引用)注意:bipush 和 sipush 中,b 和 s 不是 boolean 和 short,而是 byte 和 short。
没有类型后缀的指令
有些指令不区分类型,或者说类型由上下文决定:
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 加载 intiload_0 ~ iload_3 是 iload 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 增加 100wide 指令格式:
┌────────┬────────┬────────┐
│ 0xC4 │ index │ 可选操作数 │
└────────┴────────┴────────┘指令集设计哲学
JVM 指令集的设计有几个核心理念:
1. 精简指令集
JVM 只有约 200 条指令,比真实 CPU 的指令集少得多。许多复杂操作需要多条简单指令组合。
java
// 源码
a = b + c;
// 字节码(4 条指令)
iload_1 // 加载 b
iload_2 // 加载 c
iadd // 相加
istore_0 // 存入 a2. 基于栈而非寄存器
简化了虚拟机的实现,移植性更好,但也意味着更多内存访问(数据要在栈和局部变量之间来回搬)。
3. 字节码不绑定特定 CPU
一次编译,到处执行。代价是需要 JVM 做翻译工作(解释执行或 JIT 编译)。
本节小结
字节码指令的核心要点:
| 维度 | 说明 |
|---|---|
| 操作码 | 1 字节,0~255,最多 256 种指令 |
| 数据类型 | int/long/float/double/reference 为主 |
| 寻址方式 | 立即数、局部变量、常量池 |
| 指令分类 | 加载存储、算术、类型转换、对象/数组、方法调用、返回、栈操作、控制流、异常、同步 |
| 设计哲学 | 精简、基于栈、跨平台 |
下一节,我们来看 加载存储指令(局部变量/常量入栈)。
