基于 Oracle 官方《The Java Virtual Machine Specification》第 6 章(Java SE 26)编写。
这是什么,为什么值得懂
读懂本章,你能:
- 看
javap -c输出时,知道0xb6为什么写成invokevirtual、它在栈上怎么取参数; - 理解「为什么
long/double在栈上占一项、但某些指令要成对处理」; - 真正搞懂
i++背后是iinc,x = a + b背后是iload iload iadd istore; - 在排查诡异的
StackOverflowError、VerifyError时直达字节码层。
1. 指令的物理本质
- opcode 只占 1 字节(8 位),因此最多 256 种操作码。这就是 JVM 指令集「精简」的根本原因。
- 一条指令 = 1 字节 opcode + 可选操作数 ;操作数长度因指令而异,所以指令长度可变。
- 操作数大多从操作数栈隐式取得(第 2 章讲过的栈机器模型),而非寄存器编号------这让编译产物紧凑。
- 少数操作数在编译期 就嵌入指令(如
iload 2里的2是局部变量槽号,ldc #7里的#7是常量池索引)。
1.1 指令描述记法
每条指令条目按统一格式书写:
操作码(mnemonic)
操作码值:十进制 (0x十六进制)
Forms(家族成员与各自 opcode,如 iconst_<i>)
操作数栈图:..., in1, in2 → ..., out
描述
异常(链接/运行期,此外仅允许 VirtualMachineError 子类)
操作数栈记法 :栈从左向右增长,栈顶在最右。..., value1, value2 → ..., result 表示执行前 value2 在栈顶、value1 次之,执行后两者弹出、压入 result。long/double 在栈上算一项(但占两个字宽)。
1.2 保留操作码与虚拟机错误
- 保留操作码 (不出现在合法 class 文件里):
0xcabreakpoint、0xfeimpdep1、0xffimpdep2------仅供实现内部调试/扩展用。 wide指令 (0xc4):为操作数扩成 2 字节,用于访问索引 > 255 的局部变量或常量池项。VirtualMachineError子类 (指令仅允许抛这些):InternalError、OutOfMemoryError、StackOverflowError、UnknownError。
2. 常量与压栈(Constants)
把常量/引用压入操作数栈。小常量有「快车道」专用操作码(第 3 章 useManyNumeric 示例里见过)。
| 指令 | opcode | 格式 | 操作数栈 | 说明 |
|---|---|---|---|---|
aconst_null |
0x01 | --- | → null |
压入 null 引用 |
iconst_m1 |
0x02 | --- | → -1 |
int -1 |
iconst_<i> |
0x03--0x08 | --- | → i (i = 0...5) |
int 0~5(每值一个 opcode) |
lconst_<l> |
0x09--0x0a | --- | → l (l = 0,1) |
long 0/1 |
fconst_<f> |
0x0b--0x0d | --- | → f (0/1/2) |
float 0/1/2 |
dconst_<d> |
0x0e--0x0f | --- | → d (0/1) |
double 0/1 |
bipush |
0x10 | byte |
→ value |
压入 1 字节有符号立即数 |
sipush |
0x11 | short |
→ value |
压入 2 字节立即数 |
ldc |
0x12 | index(1B) |
→ value |
从常量池(#index)加载(int/float/String/类) |
ldc_w |
0x13 | index(2B) |
→ value |
同 ldc,索引 2 字节 |
ldc2_w |
0x14 | index(2B) |
→ value |
加载 long/double(第 3、4 章讲过的双槽常量) |
记忆法:
i/l/f/d/a分别是 int/long/float/double/reference 的前缀;const是常量,push是立即数,ldc是 load constant(从常量池)。
3. 局部变量加载与存储(Loads / Stores)
在局部变量表 (第 2 章 §2.6,槽位 0 是 this)与操作数栈之间搬运值。
| 指令 | opcode | 格式 | 操作数栈 | 说明 |
|---|---|---|---|---|
iload |
0x15 | index |
→ value |
加载 int 局部变量(槽 index) |
iload_<n> |
0x1a--0x1d | --- | → value (n=0...3) |
加载槽 0~3 的 int(省 1 字节操作数) |
lload |
0x16 | index |
→ value |
加载 long(占 2 槽) |
fload |
0x17 | index |
→ value |
加载 float |
dload |
0x18 | index |
→ value |
加载 double(占 2 槽) |
aload |
0x19 | index |
→ value |
加载引用(对象/数组) |
istore |
0x36 | index |
value → |
存 int 到局部变量 |
istore_<n> |
0x3b--0x3e | --- | value → (n=0...3) |
存槽 0~3 的 int |
lstore/fstore/dstore/astore |
0x37/0x38/0x39/0x3a | index |
value → |
对应类型存储 |
数组元素加载/存储(下标在栈上):
| 指令 | opcode | 操作数栈 | 说明 |
|---|---|---|---|
iaload |
0x2e | arrayref, index → value |
int 数组取元素 |
iaload/faload/baload/caload/saload |
0x2e--0x35 | 同构 | 各基本类型数组取元素 |
aaload |
0x32 | arrayref, index → value |
引用数组取元素(§6.5 示例:弹出引用数组与 int 索引,取组件引用压栈) |
iastore/fastore/bastore/castore/sastore/aastore |
0x4f--0x53 | arrayref, index, value → |
各类型数组存元素 |
家族规律:
a=reference、i=int、l=long、f=float、d=double、b=byte/boolean、c=char、s=short;load=取、store=存、aload=a+load。
4. 算术运算(Arithmetic)
全部在操作数栈上完成。整数用补码、浮点用 IEEE 754(第 2 章)。
| 指令 | opcode | 操作数栈 | 说明 |
|---|---|---|---|
iadd |
0x60 | value1, value2 → result |
int 加 |
ladd |
0x61 | value1, value2 → result |
long 加 |
fadd |
0x62 | → result |
float 加 |
dadd |
0x63 | → result |
double 加 |
isub |
0x64 | value1, value2 → result |
int 减(value1 - value2) |
imul |
0x68 | → result |
int 乘 |
idiv |
0x6c | value1, value2 → result |
int 除(抛 ArithmeticException 当除 0) |
irem |
0x70 | → result |
int 取余 |
ineg |
0x74 | value → result |
int 取负 |
ishl/ishr/iushr |
0x78/0x7a/0x7c | value1, value2 → result |
左移/算术右移/逻辑右移 |
iand/ior/ixor |
0x7e/0x80/0x82 | → result |
按位与/或/异或 |
对应 l/f/d 家族 |
0x61~0x83 | 同构 | long/float/double 算术 |
idiv/ldiv抛ArithmeticException(属于运行期检查异常,第 2 章讲过)。iinc(见下)是唯一个「直接修改局部变量、不进出栈」的算术指令。
iinc (0x84):index const → 把局部变量 index 的 int 原地加 const (value → value+const)。这是 i++ 的高效实现,不碰操作数栈。
5. 类型转换(Type Conversion)
| 指令 | opcode | 操作数栈 | 说明 |
|---|---|---|---|
i2l |
0x85 | value → result |
int → long |
i2f |
0x86 | → result |
int → float |
i2d |
0x87 | → result |
int → double |
l2i |
0x88 | → result |
long → int(截断) |
l2f/l2d |
0x89/0x8a | → result |
long → float/double |
f2i/f2l/f2d |
0x8b--0x8d | → result |
float 转换 |
d2i/d2l/d2f |
0x8e--0x90 | → result |
double 转换 |
i2b |
0x91 | value → result |
int → byte(截断第 2 章讲过) |
i2c |
0x92 | → result |
int → char |
i2s |
0x93 | → result |
int → short |
i2b/i2c/i2s也用于「窄化」------配合第 3 章sspin(short 循环用i2s截断)理解。
6. 对象创建与访问(Object Creation / Access)
| 指令 | opcode | 格式 | 操作数栈 | 说明 |
|---|---|---|---|---|
new |
0xbb | index(2B) |
→ objectref |
创建对象,未初始化 (第 3 章:new+dup+invokespecial <init>) |
newarray |
0xbc | atype |
count → arrayref |
创建基本类型数组 |
anewarray |
0xbd | index(2B) |
count → arrayref |
创建引用类型数组 |
multianewarray |
0xc5 | index,dimensions |
→ arrayref |
创建多维数组(第 3 章:3D 只定前两维) |
getfield |
0xb4 | index(2B) |
objectref → value |
取实例字段(解析 §5.4.3.2) |
putfield |
0xb5 | index(2B) |
objectref, value → |
写实例字段 |
getstatic |
0xb2 | index(2B) |
→ value |
取静态字段 |
putstatic |
0xb3 | index(2B) |
value → |
写静态字段 |
arraylength |
0xbe | arrayref → length |
取数组长度 | |
instanceof |
0xc1 | index(2B) |
objectref → result |
类型判断(1/0) |
checkcast |
0xc0 | index(2B) |
objectref → objectref |
类型强转(失败 ClassCastException) |
new后对象处于「未初始化」状态,必须紧跟invokespecial <init>(第 3 章详解)。字段/静态指令的index都指向常量池里的Fieldref。
7. 操作数栈操作(Operand Stack Manipulation)
不按类型,纯栈结构操作。long/double 占一项(两条槽)。
| 指令 | opcode | 操作数栈 | 说明 |
|---|---|---|---|
pop |
0x57 | value → |
弹出栈顶 1 字宽 |
pop2 |
0x58 | value2, value1 → |
弹出 2 字宽(long/double) |
dup |
0x59 | value → value, value |
复制栈顶 |
dup_x1 |
0x5a | value2, value1 → value1, value2, value1 |
复制并插入栈顶下 1 位 |
dup_x2 |
0x5b | 复杂 | 复制插到栈顶下 2 位 |
dup2 / dup2_x1 / dup2_x2 |
0x5c--0x5e | --- | 复制 2 字宽(long/double)的变体 |
swap |
0x5f | value2, value1 → value1, value2 |
交换栈顶两字宽 |
new Object()用的就是dup------new压入未初始化对象,再dup一份供<init>消费、留一份作结果。
8. 控制转移(Control Transfer)
条件跳转的「比较」直接在栈上做,结果为分支偏移量。
| 指令 | opcode | 操作数栈 | 说明 |
|---|---|---|---|
ifeq |
0x99 | value → |
value == 0 则跳转 |
ifne |
0x9a | value → |
value != 0 则跳转 |
iflt/ifge/ifgt/ifle |
0x9b--0x9e | value → |
与 0 比较(< / ≥ / > / ≤) |
if_icmpeq |
0x9f | v1, v2 → |
两 int 相等则跳 |
if_icmpne/if_icmplt/if_icmpge/if_icmpgt/if_icmple |
0xa0--0xa4 | v1, v2 → |
两 int 比较跳转 |
if_acmpeq/if_acmpne |
0xa5/0xa6 | v1, v2 → |
两引用相等/不等则跳 |
ifnull/ifnonnull |
0xc6/0xc7 | value → |
引用为 null/非 null 则跳 |
goto |
0xa7 | branch(2B) |
无条件跳 |
goto_w |
0xc8 | branch(4B) |
无条件跳(大范围) |
tableswitch |
0xaa | index → |
密集 switch(第 3 章讲过的「快表」) |
lookupswitch |
0xab | index → |
稀疏 switch(排序键值对) |
jsr/jsr_w/ret |
0xa8/0xc9/0xa9 | --- | 旧版 子例程(finally 用,已弃用,对应第 2 章 returnAddress) |
第 3 章的
while循环就是「底部测试 +goto跳过首轮」模式;switch按密集度选tableswitch/lookupswitch。
9. 方法调用与返回(Method Invocation / Return)
| 指令 | opcode | 格式 | 操作数栈 | 说明 |
|---|---|---|---|---|
invokevirtual |
0xb6 | index(2B) |
objref, [args] → result |
实例虚方法(按实际类型分派) |
invokespecial |
0xb7 | index(2B) |
objref, [args] → result |
父类方法/<init>/私有方法 |
invokestatic |
0xb8 | index(2B) |
[args] → result |
静态方法 |
invokeinterface |
0xb9 | index,count,0 |
objref, [args] → result |
接口方法 |
invokedynamic |
0xba | index,0,0 |
[args] → result |
动态调用(lambda/引导方法,§5.4.3.6) |
ireturn |
0xac | value → [empty] |
返回 int | |
lreturn/freturn/dreturn |
0xad--0xaf | value → |
返回 long/float/double | |
areturn |
0xb0 | value → |
返回引用 | |
return |
0xb1 | → [empty] |
返回 void |
四个
invoke*的区别是第 3 章的重点:invokespecial调<init>、invokestatic调静态、invokevirtual调普通实例方法、invokeinterface调接口。调用前参数(和objref)须按描述符顺序在栈上排好。
10. 异常与同步(Throwing / Synchronization)
| 指令 | opcode | 操作数栈 | 说明 |
|---|---|---|---|
athrow |
0xbf | objectref → objectref |
抛出异常(第 3 章:构造 Throwable 后 athrow) |
monitorenter |
0xc2 | objectref → |
进入对象监视器(加锁) |
monitorexit |
0xc3 | objectref → |
退出对象监视器(解锁) |
synchronized块编译为monitorenter+monitorexit,并用异常表保证异常时也解锁。athrow的异常处理由Code里的异常表指引。
11. 其他
| 指令 | opcode | 说明 |
|---|---|---|
nop |
0x00 | 空操作(占位/对齐) |
wide |
0xc4 | 扩展后续指令操作数为 2 字节 |
12. 一个综合例子:拆解 x = a + b
源码:
java
void add() {
int x = a + b; // 假设 a, b 是 int 字段,x 是局部变量
}
javap -c 可能这样反汇编(概念示意):
0: aload_0 // 压入 this(字段在对象上)
1: getfield #2 // 取字段 a 压栈
4: aload_0 // 压入 this
5: getfield #3 // 取字段 b 压栈
8: iadd // 弹出 a,b,压入 a+b
9: istore_1 // 存到局部变量 x(槽 1)
对应 opcode 流:0x2a 0xb4 0x?? 0x2a 0xb4 0x?? 0x60 0x3c------0x2a=aload_0,0xb4=getfield,0x60=iadd,0x3c=istore_1。
13. 动手:用 javap -c 把玩真实字节码
写个含循环、分支、异常的小方法:
java
public class Bytecode {
public static int sum(int n) {
int s = 0;
for (int i = 1; i <= n; i++) s += i;
return s;
}
}
bash
javac Bytecode.java
javap -c Bytecode.class
小结
- 指令 = 1 字节 opcode + 可选操作数,长度可变,操作数多从栈取;
- 记法统一:mnemonic + opcode 十六进制 + 栈图 + 异常;
- 按前缀分族(
i/l/f/d/a+load/store/const/add...),同一类操作 opcode 连续编号; long/double占栈一项(2 字宽),iinc是唯一原地改局部变量的算术指令;new/dup/invokespecial <init>是对象创建固定三件套。