JVM - 字节码指令集与解析
# 1. 概述:字节码指令的基础
官方文档参考: JVMS 8 - Chapter 6: The Java Virtual Machine Instruction Set (opens new window)
# 1.1 字节码:虚拟机的“汇编语言”
Java 字节码(Bytecode)是 Java 虚拟机 (JVM) 执行的指令集。可以将其类比为物理计算机的汇编语言,是 JVM 能够理解和执行的最底层、最基本的命令。
# 1.2 指令结构:操作码与操作数
- 基本构成: JVM 指令由一个单字节长度的操作码 (Opcode) 和零至多个操作数 (Operands) 构成。
- 操作码 (Opcode): 代表一种特定的操作含义,是一个 0 到 255 之间的数字。由于长度限制为 1 字节,JVM 的指令总数不超过 256 条。
- 操作数 (Operands): 跟随在操作码之后,提供该操作所需的参数。
- 基于操作数栈: JVM 采用面向操作数栈的架构,而不是基于寄存器的架构。这意味着大多数指令直接在操作数栈上工作(弹栈、运算、压栈),因此很多指令只有一个操作码,没有显式的操作数。操作数通常隐含在指令本身或从操作数栈中获取。
# 1.3 学习字节码的价值
熟悉 JVM 指令对于以下场景至关重要:
- 动态字节码生成: 使用 ASM、Javassist 等库在运行时创建或修改类。
- 反编译 Class 文件: 理解
javap
或其他反编译工具的输出。 - Class 文件修复与分析: 对字节码进行底层分析和修改。
- 深入理解 JVM: 理解 Java 代码的底层执行逻辑、性能瓶颈。
因此,阅读和理解字节码是掌握 Java 虚拟机的基础技能。
# 1.4 简化的 JVM 执行模型
如果不考虑复杂的异常处理和 JIT 编译优化,JVM 解释器的基本执行模型可以简化为以下循环:
do {
// 1. 自动递增程序计数器 (PC Register)
pc++;
// 2. 根据 PC 指示的位置,从字节码流中取出当前指令的操作码
opcode = bytecode_stream[pc];
// 3. 如果指令有操作数,则从字节码流中取出操作数
if (opcode_has_operands(opcode)) {
operands = bytecode_stream[pc+1 ...];
pc += length_of_operands; // 更新 PC 跳过操作数
}
// 4. 执行操作码定义的操作(可能会修改操作数栈、局部变量表等)
execute_operation(opcode, operands);
} while (more_bytecode_exists());
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1.5 字节码与数据类型
JVM 指令集的设计与 Java 的基本数据类型紧密相关。
- 类型指示符: 大多数指令的操作码助记符 (Mnemonic) 中包含了表明其操作数据类型的特殊字符:
i
:int
l
:long
s
:short
b
:byte
c
:char
f
:float
d
:double
a
:reference
(对象引用)- 例如:
iload
(加载 int),fstore
(存储 float),ladd
(long 型加法),areturn
(返回引用)。
- 无类型指示符:
- 某些指令(如
arraylength
)的操作数类型是固定的(如数组引用),无需类型指示符。 - 某些指令(如无条件跳转
goto
)与数据类型无关。
- 某些指令(如
- 对
byte
,char
,short
,boolean
的处理:- JVM 指令集中没有直接支持
boolean
,byte
,char
,short
类型的专用算术、加载、存储指令(除了特定的数组操作指令)。 - 编译器处理: 在编译期或运行期,这些类型会被扩展 (Sign-Extend 或 Zero-Extend) 为相应的
int
类型进行处理。byte
,short
->int
(带符号扩展)boolean
,char
->int
(零位扩展,boolean
true=1, false=0)
- 运算: 对这些类型的运算实际上是使用相应的
int
类型指令完成的。 - 数组: 处理
boolean[]
,byte[]
,char[]
,short[]
数组时,也使用int
类型的相关字节码指令(如baload
对应byte
/boolean
数组加载,但内部可能按int
处理)。
- JVM 指令集中没有直接支持
# 1.6 指令分类
为了便于学习,JVM 字节码指令可以按用途大致分为 9 类:
- 加载与存储指令 (Load and Store Instructions): 在局部变量表和操作数栈之间传输数据。
- 算术指令 (Arithmetic Instructions): 执行数值运算。
- 类型转换指令 (Type Conversion Instructions): 在不同数值类型间转换。
- 对象的创建与访问指令 (Object Creation and Manipulation Instructions): 创建对象/数组,访问字段/数组元素。
- 方法调用与返回指令 (Method Invocation and Return Instructions): 调用方法,从方法返回。
- 操作数栈管理指令 (Operand Stack Management Instructions): 直接操作操作数栈(如出栈、复制、交换)。
- 控制转移指令 (Control Transfer Instructions): 条件/无条件跳转,
switch
语句。 - 异常处理指令 (Exception Handling Instructions): 抛出异常 (
athrow
)。异常捕获由异常表处理。 - 同步控制指令 (Synchronization Instructions): 支持
synchronized
关键字 (monitorenter
,monitorexit
)。
核心交互:
- 指令可以从多个来源获取数据(局部变量表、常量池、堆对象、方法调用结果等),并将数据压入操作数栈。
- 指令也可以从操作数栈弹出数据,用于赋值给局部变量、进行运算、作为方法参数、执行系统调用等。
# 2. 加载与存储指令
这类指令负责在栈帧的局部变量表 (Local Variable Table) 和操作数栈 (Operand Stack) 之间来回传递数据。
# 2.1 常用指令概览
- 局部变量 -> 操作数栈 (加载 Load):
xload index
: 从指定索引index
的局部变量加载。(iload
,lload
,fload
,dload
,aload
)xload_<n>
: 从索引 0 到 3 的局部变量加载,n
隐含在指令中。(iload_0
,lload_1
,fload_2
,dload_3
,aload_0
等)xaload
: 从数组加载元素到操作数栈。(baload
,caload
,saload
,iaload
,laload
,faload
,daload
,aaload
)
- 常量 -> 操作数栈 (加载 Load Constant):
bipush byte
: 将单字节常量 (-128~127) 推送入栈。sipush short
: 将双字节常量 (-32768~32767) 推送入栈。ldc index
: 从常量池加载int
,float
,String
或Class
引用 (单字节索引)。ldc_w index
: 功能同ldc
,使用双字节索引。ldc2_w index
: 从常量池加载long
或double
(双字节索引)。aconst_null
: 将null
推送入栈。iconst_m1
,iconst_<i>
: 将 int 型 -1, 0, 1, 2, 3, 4, 5 推送入栈。lconst_<l>
: 将 long 型 0, 1 推送入栈。fconst_<f>
: 将 float 型 0, 1, 2 推送入栈。dconst_<d>
: 将 double 型 0, 1 推送入栈。
- 操作数栈 -> 局部变量表 (存储 Store):
xstore index
: 将栈顶元素存储到指定索引index
的局部变量。(istore
,lstore
,fstore
,dstore
,astore
)xstore_<n>
: 将栈顶元素存储到索引 0 到 3 的局部变量。(istore_0
,lstore_1
,fstore_2
,dstore_3
,astore_0
等)xastore
: 将栈顶元素存储到数组的指定索引位置。(bastore
,castore
,sastore
,iastore
,lastore
,fastore
,dastore
,aastore
)
- 局部变量表索引扩展:
wide
: 用于扩展局部变量索引或iinc
指令的操作数,使其支持大于 255 的索引或增量。
xload_<n>
和 xstore_<n>
的说明:
这些是特定索引(0-3)的优化指令。它们的操作数(局部变量索引)隐含在操作码中,无需额外读取操作数,执行更快,字节码更紧凑。语义上 iload_1
等同于 iload 1
。
# 2.2 操作数栈与局部变量表再探
- 操作数栈 (Operand Stack):
- 每个方法执行时在栈帧内创建。
- 是一个后进先出 (LIFO) 的栈。
- 用于存放方法执行过程中的操作数和中间结果。
- 指令执行前,所需操作数压栈;执行时,操作数弹栈;执行后,结果压栈。
- 栈的深度在编译期确定,记录在 Code 属性的
max_stack
中。
- 局部变量表 (Local Variable Table):
- 每个方法执行时在栈帧内创建。
- 本质是一个数组,用于存储方法参数和方法体内定义的局部变量。
- 容量以槽 (Slot) 为单位。
boolean
,byte
,char
,short
,int
,float
,reference
类型占用 1 个 Slot;long
和double
类型占用 2 个连续的 Slot。 - 索引:
- 对于实例方法 (非 static),第 0 个 Slot 固定存储当前对象的引用
this
。 - 之后依次存放方法参数。
- 再之后存放方法体内的局部变量。
- 对于实例方法 (非 static),第 0 个 Slot 固定存储当前对象的引用
- Slot 复用: 方法内作用域不重叠的局部变量可能会复用同一个 Slot,以节省空间。
- 局部变量表的容量在编译期确定,记录在 Code 属性的
max_locals
中。 - 局部变量表是 GC Roots 的重要组成部分。
示例: 局部变量表布局
// 文件名: LocalVarExample.java
public class LocalVarExample {
public void foo(long l, float f) { // 实例方法
// Slot 0: this (类型 LocalVarExample)
// Slot 1, 2: l (long 类型,占两个槽)
// Slot 3: f (float 类型)
{
int i = 0; // Slot 4: i (int 类型)
} // i 的作用域结束,Slot 4 可被复用
{
String s = "Hello"; // Slot 4: s (String 引用类型),复用了 Slot 4
} // s 的作用域结束
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 2.3 局部变量压栈指令详解
将局部变量表中的值加载到操作数栈顶。
xload_<n>
: (x=i, l, f, d, a; n=0, 1, 2, 3)iload_0
: 加载第 0 个 Slot 的 int 值。aload_0
: 加载第 0 个 Slot 的引用值 (通常是this
)。lload_1
: 加载第 1, 2 个 Slot 的 long 值。
xload index
: (x=i, l, f, d, a)iload 4
: 加载第 4 个 Slot 的 int 值。aload 5
: 加载第 5 个 Slot 的引用值。
代码示例:
// 文件名: LoadTest.java
public class LoadTest {
// 参数在局部变量表中的布局:
// Slot 0: this
// Slot 1: num (int)
// Slot 2: obj (Object ref)
// Slot 3, 4: count (long)
// Slot 5: flag (boolean -> int)
// Slot 6: arr (short[] ref)
public void load(int num, Object obj, long count, boolean flag, short[] arr) {
// 访问 num (Slot 1)
System.out.println(num);
// 访问 obj (Slot 2)
System.out.println(obj);
// 访问 count (Slot 3)
System.out.println(count);
// 访问 flag (Slot 5)
System.out.println(flag);
// 访问 arr (Slot 6)
System.out.println(arr);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
对应字节码 (javap -v LoadTest.class
):
// ... 省略 System.out 获取 ...
// System.out.println(num);
3: iload_1 // 加载 Slot 1 (num) 的 int 值到操作数栈
// ... 调用 println(I) ...
// System.out.println(obj);
7: aload_2 // 加载 Slot 2 (obj) 的引用值到操作数栈
// ... 调用 println(Ljava/lang/Object;) ...
// System.out.println(count);
11: lload_3 // 加载 Slot 3, 4 (count) 的 long 值到操作数栈
// ... 调用 println(J) ...
// System.out.println(flag);
15: iload 5 // 加载 Slot 5 (flag) 的 int 值 (boolean 视为 int) 到操作数栈
// ... 调用 println(Z) ... (println 对 boolean 有重载)
// System.out.println(arr);
19: aload 6 // 加载 Slot 6 (arr) 的引用值到操作数栈
// ... 调用 println(Ljava/lang/Object;) ...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 2.4 常量入栈指令详解
将常量值推送到操作数栈顶。
const
系列: 用于加载特定的小范围常量或null
,指令本身隐含了常量值。iconst_m1
(-1),iconst_0
(0), ...,iconst_5
(5)lconst_0
(0L),lconst_1
(1L)fconst_0
(0.0f),fconst_1
(1.0f),fconst_2
(2.0f)dconst_0
(0.0d),dconst_1
(1.0d)aconst_null
(null
)
push
系列: 用于加载更大范围的整数常量。bipush byte
: 加载一个字节表示的 int (-128 ~ 127)。操作数是 1 字节。sipush short
: 加载一个短整型表示的 int (-32768 ~ 32767)。操作数是 2 字节。
ldc
系列: 从常量池加载。ldc index
: 加载常量池中索引为index
(1 字节) 的项,可以是int
,float
,String
(字面量引用),Class
引用,MethodType
,MethodHandle
。ldc_w index
: 功能同ldc
,但索引index
是 2 字节,支持更大的常量池。ldc2_w index
: 加载常量池中索引为index
(2 字节) 的long
或double
值。
常量加载指令选择优先级 (以 int 为例):
- -1 到 5:
iconst_<i>
- -128 到 127:
bipush
- -32768 到 32767:
sipush
- 其他 int 值:
ldc
(值存放在常量池)
代码示例 1: int 常量加载
// 文件名: PushConstLdcTest.java
public class PushConstLdcTest {
public void pushConstLdc() {
int i = -1; // iconst_m1
int a = 5; // iconst_5
int b = 6; // bipush 6
int c = 127; // bipush 127
int d = 128; // sipush 128
int e = 32767; // sipush 32767
int f = 32768; // ldc (从常量池加载)
}
}
2
3
4
5
6
7
8
9
10
11
12
对应字节码:
0: iconst_m1
1: istore_1 // i = -1
2: iconst_5
3: istore_2 // a = 5
4: bipush 6
6: istore_3 // b = 6
7: bipush 127
9: istore 4 // c = 127
11: sipush 128
14: istore 5 // d = 128
16: sipush 32767
19: istore 6 // e = 32767
21: ldc #2 // 从常量池加载 int 32768 (假设 #2 是 Integer 32768)
23: istore 7 // f = 32768
25: return
2
3
4
5
6
7
8
9
10
11
12
13
14
15
代码示例 2: 其他类型常量加载
// 文件名: ConstLdcTest.java
import java.util.Date;
public class ConstLdcTest {
public void constLdc() {
long a1 = 1L; // lconst_1
long a2 = 2L; // ldc2_w (从常量池加载 long 2)
float b1 = 2f; // fconst_2
float b2 = 3f; // ldc (从常量池加载 float 3.0)
double c1 = 1d; // dconst_1
double c2 = 2d; // ldc2_w (从常量池加载 double 2.0)
Date d = null; // aconst_null
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
对应字节码:
0: lconst_1
1: lstore_1 // a1 = 1L
2: ldc2_w #2 // lstore_3 (假设 #2 是 Long 2)
5: lstore_3 // a2 = 2L
6: fconst_2
7: fstore 5 // b1 = 2f
9: ldc #4 // fstore 6 (假设 #4 是 Float 3.0f)
11: fstore 6 // b2 = 3f
13: dconst_1
14: dstore 7 // c1 = 1d
16: ldc2_w #5 // dstore 9 (假设 #5 是 Double 2.0d)
19: dstore 9 // c2 = 2d
21: aconst_null
22: astore 11 // d = null
24: return
2
3
4
5
6
7
8
9
10
11
12
13
14
15
常量指令总结表:
类型 | 常量值范围 | 指令 |
---|---|---|
int/byte/char/short/boolean | -1 到 5 | iconst_<i> |
-128 到 127 | bipush | |
-32768 到 32767 | sipush | |
其他 int | ldc /ldc_w | |
long | 0, 1 | lconst_<l> |
其他 long | ldc2_w | |
float | 0.0, 1.0, 2.0 | fconst_<f> |
其他 float | ldc /ldc_w | |
double | 0.0, 1.0 | dconst_<d> |
其他 double | ldc2_w | |
reference | null | aconst_null |
String 字面量 | ldc /ldc_w | |
Class 字面量 | ldc /ldc_w |
# 2.5 出栈装入局部变量表指令详解
将操作数栈顶的一个或两个元素弹出,并存储到局部变量表的指定 Slot 中。常用于赋值操作。
xstore_<n>
: (x=i, l, f, d, a; n=0, 1, 2, 3)istore_1
: 将栈顶 int 存入 Slot 1。astore_0
: 将栈顶引用存入 Slot 0 (通常是this
的赋值)。dstore_2
: 将栈顶 double (占两个 Slot) 存入 Slot 2 和 3。
xstore index
: (x=i, l, f, d, a)istore 4
: 将栈顶 int 存入 Slot 4。lstore 5
: 将栈顶 long 存入 Slot 5 和 6。
xastore
: 将操作数栈顶的值存储到数组的指定索引位置。- 执行前栈结构 (从栈顶往下):
value
,index
,arrayref
- 执行后: 栈顶三个元素弹出,
arrayref[index] = value
。
- 执行前栈结构 (从栈顶往下):
代码示例:
// 文件名: StoreTest.java
public class StoreTest {
// 参数 k 在 Slot 1 (int), d 在 Slot 2, 3 (double)
public void store(int k, double d) {
// Slot 4: m (int)
int m = k + 2;
// Slot 5, 6: l (long)
long l = 12L;
// Slot 7: str (String ref)
String str = "kele";
// Slot 8: f (float)
float f = 10.0F;
// 重新给参数 d 赋值 (Slot 2, 3)
d = 10.0;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
对应字节码:
// int m = k + 2;
0: iload_1 // 加载 k (Slot 1)
1: iconst_2 // 加载常量 2
2: iadd // k + 2
3: istore 4 // 将结果存入 m (Slot 4)
// long l = 12L;
5: ldc2_w #2 // 加载 long 12L (假设 #2 是 Long 12)
8: lstore 5 // 将结果存入 l (Slot 5, 6)
// String str = "kele";
10: ldc #4 // 加载 "kele" (假设 #4 是 String "kele")
12: astore 7 // 将引用存入 str (Slot 7)
// float f = 10.0F;
14: ldc #5 // 加载 10.0f (假设 #5 是 Float 10.0f)
16: fstore 8 // 将结果存入 f (Slot 8)
// d = 10.0;
18: ldc2_w #6 // 加载 10.0d (假设 #6 是 Double 10.0d)
21: dstore_2 // 将结果存入 d (Slot 2, 3)
22: return
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 3. 算术指令
用于对操作数栈上的两个数值进行运算,并将结果压回操作数栈。
# 3.1 分类与类型支持
- 两大类: 整数运算指令 和 浮点数运算指令。
- 整数运算:
- 针对
int
和long
类型有专用指令 (如iadd
,ladd
)。 - 不支持 直接对
byte
,short
,char
,boolean
的运算。这些类型在运算前会被提升 (Promote) 为int
,然后使用int
的算术指令进行运算。
- 针对
- 浮点数运算:
- 针对
float
和double
类型有专用指令 (如fadd
,dadd
)。 - 遵循 IEEE 754 标准。
- 运算结果可能产生
Infinity
(无穷大) 或NaN
(Not a Number)。
- 针对
# 3.2 运算溢出
- 整数运算:
- 除了除法 (
idiv
,ldiv
) 和求余 (irem
,lrem
) 在除数为 0 时会抛出ArithmeticException
外,其他整数运算(加减乘、位移)即使发生溢出(结果超出类型表示范围),也不会抛出异常,结果会进行环绕 (Wrap-around),例如Integer.MAX_VALUE + 1
结果为Integer.MIN_VALUE
。
- 除了除法 (
- 浮点数运算:
- 溢出通常产生有符号的无穷大 (
Infinity
或-Infinity
)。 - 某些未定义的操作(如
0.0 / 0.0
,sqrt(-1.0)
) 产生NaN
。 - 任何涉及
NaN
的运算,结果都是NaN
。 - 浮点运算不会抛出运行时异常 (如
ArithmeticException
)。
- 溢出通常产生有符号的无穷大 (
# 3.3 舍入模式
- 向最接近数舍入 (Round to Nearest): JVM 进行浮点数运算时,非精确结果必须舍入到最接近的可表示值。如果距离两个可表示值相等,则选择最低有效位为 0 的那个(这符合 IEEE 754 的默认舍入模式)。
- 向零舍入 (Round towards Zero): 将浮点数转换为整数时(如
f2i
,d2l
),采用此模式。结果是绝对值不大于原浮点数的最接近整数。
代码示例:浮点数特殊值
// 文件名: FloatSpecialValues.java
public class FloatSpecialValues {
public void method() {
int i = 10;
// int / 0.0 -> Infinity (除数是浮点数 0.0)
double j = i / 0.0;
System.out.println(j); // 输出 Infinity
double d1 = 0.0;
// 0.0 / 0.0 -> NaN
double d2 = d1 / 0.0;
System.out.println(d2); // 输出 NaN
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 3.4 所有算术指令列表
运算类别 | 操作 | int (byte,short,char,bool) | long | float | double |
---|---|---|---|---|---|
加法 | + | iadd | ladd | fadd | dadd |
减法 | - | isub | lsub | fsub | dsub |
乘法 | * | imul | lmul | fmul | dmul |
除法 | / | idiv | ldiv | fdiv | ddiv |
求余/取模 | % | irem | lrem | frem | drem |
取反/取负 | - (一元) | ineg | lneg | fneg | dneg |
自增 | += c | iinc index by const | - | - | - |
位运算 | 左移 << | ishl | lshl | - | - |
右移 >> | ishr (算术) | lshr | - | - | |
无符号右移>>> | iushr | lushr | - | - | |
按位或 \| | ior | lor | - | - | |
按位与 & | iand | land | - | - | |
按位异或 ^ | ixor | lxor | - | - | |
比较 (非布尔结果) | 比较 | - | lcmp | fcmpg /fcmpl | dcmpg /dcmpl |
说明:
- 比较指令 (
lcmp
,fcmp*
,dcmp*
) 的结果不是 boolean,而是int
值 (-1, 0, 或 1),通常用于后续的条件跳转。 iinc
指令比较特殊,它直接修改局部变量表中的值,而不是操作数栈。iinc index by const
表示将索引为index
的局部变量增加const
(一个带符号字节)。
# 3.5 算术指令代码示例
示例 1: 取反指令 (fneg
)
// 文件名: NegationTest.java
public class NegationTest {
public void method() {
float i = 10f; // ldc, fstore_1
float j = -i; // fload_1, fneg, fstore_2
i = -j; // fload_2, fneg, fstore_1
}
}
2
3
4
5
6
7
8
示例 2: 加法指令 (fadd
)
// 文件名: AddTest.java
public class AddTest {
public void method() {
float i = 100f; // ldc, fstore_1
i = i + 10f; // fload_1, ldc, fadd, fstore_1
}
}
2
3
4
5
6
7
示例 3: 自增指令 (iinc
)
// 文件名: IncrementTest.java
public class IncrementTest {
public void method() {
int i = 100; // bipush, istore_1
i += 10; // iinc 1 by 10 (直接修改 Slot 1 的值,效率更高)
}
}
2
3
4
5
6
7
对比示例 2,
iinc
指令更简洁高效。
示例 4: 复合运算
// 文件名: CompoundCalc.java
public class CompoundCalc {
public int method() {
int a = 80; // bipush 80, istore_1
int b = 7; // bipush 7, istore_2
int c = 10; // bipush 10, istore_3
// (a + b) * c
return (a + b) * c;
// iload_1 (a)
// iload_2 (b)
// iadd (a+b)
// iload_3 (c)
// imul ((a+b)*c)
// ireturn
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
示例 5: 位运算
// 文件名: BitwiseOp.java
public class BitwiseOp {
// 参数 i 在 Slot 1, j 在 Slot 2
public int method(int i, int j) {
// (i + j - 1) & ~(j - 1)
return ((i + j - 1) & ~(j - 1));
// iload_1 (i)
// iload_2 (j)
// iadd (i+j)
// iconst_1
// isub (i+j-1)
// iload_2 (j)
// iconst_1
// isub (j-1)
// iconst_m1 (-1, 用于按位取反)
// ixor (~(j-1), 因为 ~x 等价于 x ^ -1)
// iand ((i+j-1) & ~(j-1))
// ireturn
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
示例 6: 混合运算顺序
// 文件名: MixedOrder.java
public class MixedOrder {
public static int bar(int i) { // i 在 Slot 0 (静态方法无 this)
// ((i + 1) - 2) * 3 / 4
return ((i + 1) - 2) * 3 / 4;
// iload_0 (i)
// iconst_1
// iadd (i+1)
// iconst_2
// isub ((i+1)-2)
// iconst_3
// imul (((i+1)-2)*3)
// iconst_4
// idiv ((((i+1)-2)*3)/4)
// ireturn
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
示例 7: byte
类型运算 (提升为 int
)
// 文件名: ByteAdd.java
public class ByteAdd {
public void add() {
byte i = 15; // bipush 15, istore_1 (存入 int Slot)
int j = 8; // bipush 8, istore_2
// i + j (实际是 int + int)
int k = i + j;
// iload_1 (加载 i, 已是 int)
// iload_2 (加载 j)
// iadd
// istore_3 (存入 k)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
示例 8: i++
vs ++i
// 文件名: IncrementCompare.java
public class IncrementCompare {
public void method() {
int i = 10; // bipush 10, istore_1
// a = i++ (先用 i 的旧值,i 再自增)
int a = i++;
// iload_1 (加载 i=10)
// iinc 1 by 1 (i 变为 11)
// istore_2 (将旧值 10 存入 a)
int j = 20; // bipush 20, istore_3
// b = ++j (j 先自增,再用 j 的新值)
int b = ++j;
// iinc 3 by 1 (j 变为 21)
// iload_3 (加载 j=21)
// istore 4 (将新值 21 存入 b)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
示例 9: i = i++
的陷阱
// 文件名: IncrementTrap.java
public class IncrementTrap {
public void method() {
int i = 10; // bipush 10, istore_1
// i = i++;
// 1. 加载 i 的当前值 (10) 到操作数栈
// iload_1
// 2. 将局部变量表中的 i 自增 1 (i 变为 11)
// iinc 1 by 1
// 3. 将操作数栈顶的值 (旧值 10) 存回局部变量 i
// istore_1 (覆盖了自增后的 11)
i = i++;
System.out.println(i); // 输出 10
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 3.6 比较指令详解 (lcmp
, fcmp*
, dcmp*
)
- 作用: 比较操作数栈顶的两个数值(
long
,float
,double
),并将比较结果(-1
,0
, 或1
)压入栈。 - 指令:
lcmp
: 比较两个long
。fcmp<op>
: 比较两个float
(fcmpg
,fcmpl
)。dcmp<op>
: 比较两个double
(dcmpg
,dcmpl
)。
- 操作数: 从栈顶弹出两个值进行比较(假设栈顶是
value2
,次顶是value1
)。 - 结果压栈:
- 若
value1 > value2
, 压入1
。 - 若
value1 == value2
, 压入0
。 - 若
value1 < value2
, 压入-1
。
- 若
- 处理
NaN
:fcmp*
和dcmp*
需要处理NaN
。fcmp**g**
/dcmp**g**
(g
for greater): 如果任一操作数是NaN
,压入1
。fcmp**l**
/dcmp**l**
(l
for less): 如果任一操作数是NaN
,压入-1
。
- 后续: 比较指令的结果通常立即被条件跳转指令(如
ifeq
,iflt
等)使用。
# 4. 类型转换指令
用于在不同的数值类型之间进行转换。
# 4.1 类型转换指令集概览
JVM 提供了多种数值类型转换指令。基本格式是 <source_type>2<target_type>
(读作 source to target)。
源类型 | 转 byte | 转 char | 转 short | 转 int | 转 long | 转 float | 转 double |
---|---|---|---|---|---|---|---|
int | i2b | i2c | i2s | - | i2l | i2f | i2d |
long | (l2i,i2b) | (l2i,i2c) | (l2i,i2s) | l2i | - | l2f | l2d |
float | (f2i,i2b) | (f2i,i2c) | (f2i,i2s) | f2i | f2l | - | f2d |
double | (d2i,i2b) | (d2i,i2c) | (d2i,i2s) | d2i | d2l | d2f | - |
注: 括号内表示需要通过中间类型 (int
) 进行转换。
# 4.2 宽化类型转换 (Widening Conversion)
从小范围类型向大范围类型的转换,通常是安全的,但也可能损失精度。
- JVM 直接支持 (无需指令):
byte
->int
short
->int
char
->int
- (这些转换在加载或运算时自动完成)
- JVM 提供指令支持:
int
->long
(i2l
)int
->float
(i2f
)int
->double
(i2d
)long
->float
(l2f
)long
->double
(l2d
)float
->double
(f2d
)
精度损失:
int
->long
,int
->double
,long
->double
: 不损失信息。int
->float
,long
->float
,long
->double
: 可能损失精度 (丢失低位有效数字),但数量级不会错。转换遵循 IEEE 754 向最接近数舍入。- 宽化转换永远不会导致运行时异常。
代码示例 1: 常见宽化转换
// 文件名: WideningTest1.java
public class WideningTest1 {
public void upCast1() {
int i = 10;
long l = i; // 隐式转换, 字节码使用 i2l
float f = i; // 隐式转换, 字节码使用 i2f
double d = i; // 隐式转换, 字节码使用 i2d
long l2 = 100L;
float f1 = l2; // 隐式转换, 字节码使用 l2f
double d1 = l2; // 隐式转换, 字节码使用 l2d
float f2 = 10.0f;
double d2 = f2; // 隐式转换, 字节码使用 f2d
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
代码示例 2: 宽化转换精度损失
// 文件名: WideningTest2.java
public class WideningTest2 {
public void upCast2() {
int i = 123123123;
float f = i; // 可能损失精度
System.out.println(f); // 输出: 1.2312312E8 (精度丢失)
long l = 123123123123123123L;
double d = l; // 可能损失精度
System.out.println(d); // 输出: 1.2312312312312312E17 (精度丢失)
}
}
2
3
4
5
6
7
8
9
10
11
12
代码示例 3: byte
/short
到 int
(无显式指令)
// 文件名: WideningTest3.java
public class WideningTest3 {
// 参数 b 在 Slot 1, s 在 Slot 2
public void upCast3(byte b, short s) {
// byte b 赋值给 int i 时,JVM 直接处理,无需 i2b 指令
int i = b; // iload_1, istore_3
// short s 赋值给 int i 时,JVM 直接处理,无需 i2s 指令
i = s; // iload_2, istore_3
}
}
// 字节码中不会出现 i2b 或 i2s 用于这种赋值。
2
3
4
5
6
7
8
9
10
11
# 4.3 窄化类型转换 (Narrowing Conversion)
从大范围类型向小范围类型的转换,可能导致信息丢失(精度、范围、符号)。需要显式强制类型转换。
- JVM 提供指令支持:
int
->byte
(i2b
)int
->char
(i2c
)int
->short
(i2s
)long
->int
(l2i
)float
->int
(f2i
)float
->long
(f2l
)double
->int
(d2i
)double
->long
(d2l
)double
->float
(d2f
)
精度与范围损失:
- 整数间窄化: 高位被截断。例如
int
转byte
只保留最低 8 位,可能导致符号改变和值环绕。 - 浮点转整数:
NaN
->0
。Infinity
/-Infinity
-> 对应整数类型的MAX_VALUE
/MIN_VALUE
。- 普通浮点数 -> 使用向零舍入得到整数。如果结果在目标整数类型范围内,则为该整数;否则为目标类型的
MAX_VALUE
或MIN_VALUE
。
double
->float
:- 可能损失精度。
- 绝对值过大 ->
Infinity
/-Infinity
。 - 绝对值过小 ->
0.0
/-0.0
。 NaN
->NaN
。
- 窄化转换永远不会导致 JVM 抛出运行时异常。
代码示例 1: 常见窄化转换
// 文件名: NarrowingTest1.java
public class NarrowingTest1 {
public void downCast1() {
int i = 10;
byte b = (byte)i; // i2b
short s = (short)i; // i2s
char c = (char)i; // i2c
long l = 10L;
int i1 = (int)l; // l2i
// long 转 byte 需要先转 int: l -> i -> b
byte b1 = (byte)l; // l2i, i2b
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
注意
byte b1 = (byte)l;
对应的字节码是 l2i
后跟 i2b
。
代码示例 2: 窄化转换值环绕
// 文件名: NarrowingTest2.java
public class NarrowingTest2 {
public void downCast2() {
int i = 128; // 1000 0000 (二进制)
// 转换为 byte 时,只保留低 8 位,即 1000 0000
// byte 的 1000 0000 表示 -128 (补码)
byte b = (byte)i; // i2b
System.out.println(b); // 输出 -128
}
}
2
3
4
5
6
7
8
9
10
代码示例 3: 浮点数窄化转换特殊值
// 文件名: NarrowingTest3.java
public class NarrowingTest3 {
public void downCast3() {
double d_nan = Double.NaN;
int i_nan = (int)d_nan; // d2i
System.out.println(d_nan); // 输出 NaN
System.out.println(i_nan); // 输出 0
double d_inf = Double.POSITIVE_INFINITY;
long l_inf = (long)d_inf; // d2l
int i_inf = (int)d_inf; // d2i
System.out.println(l_inf); // 输出 Long.MAX_VALUE (9223372036854775807)
System.out.println(i_inf); // 输出 Integer.MAX_VALUE (2147483647)
float f_inf = (float)d_inf; // d2f
System.out.println(f_inf); // 输出 Infinity
float f_nan = (float)d_nan; // d2f
System.out.println(f_nan); // 输出 NaN
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 5. 对象的创建与访问指令
JVM 对面向对象提供了丰富的字节码支持,包括对象创建、字段访问、数组操作和类型检查。
# 5.1 创建指令
new index
: 创建一个类实例。- 操作数
index
是指向常量池中CONSTANT_Class_info
的索引,表示要创建的类。 - 执行过程:
- 在堆上分配对象内存(未初始化)。
- 将新创建对象的引用压入操作数栈。
- 注意:
new
指令只负责分配内存和压入引用,对象的初始化(调用构造器<init>
)需要后续的dup
和invokespecial
指令配合完成。
- 操作数
newarray atype
: 创建基本类型数组。- 操作数
atype
是一个编码,指定数组元素的基本类型 (如 4=boolean, 8=byte, 10=int, 11=long 等)。 - 执行前,操作数栈顶需要有一个
int
值表示数组长度 (count
)。 - 执行过程:弹出
count
,在堆上创建基本类型数组,并将数组引用压入栈。
- 操作数
anewarray index
: 创建引用类型数组。- 操作数
index
是指向常量池中CONSTANT_Class_info
的索引,表示数组元素的引用类型。 - 执行前,操作数栈顶需要有一个
int
值表示数组长度 (count
)。 - 执行过程:弹出
count
,在堆上创建引用类型数组,并将数组引用压入栈。
- 操作数
multianewarray index dimensions
: 创建多维数组。index
: 指向常量池CONSTANT_Class_info
的索引,表示数组类型。dimensions
: (u1) 表示数组的维度。- 执行前,操作数栈需要有
dimensions
个int
值,表示各维度的长度。 - 执行过程:弹出所有维度长度,在堆上创建多维数组,并将数组引用压入栈。
代码示例 1: 创建类实例 (new
)
// 文件名: NewInstanceTest.java
import java.io.File;
public class NewInstanceTest {
public void newInstance() {
// 1. new Object()
Object obj = new Object();
// 字节码:
// new #2 // 创建 Object 实例, 引用入栈 (假设 #2 是 Object Class Info)
// dup // 复制栈顶引用 (一个给构造器用, 一个给赋值用)
// invokespecial #1 // 调用 Object.<init>()V 构造器 (消耗一个引用)
// astore_1 // 将另一个引用存入 obj (Slot 1)
// 2. new File("kele")
File file = new File("kele");
// 字节码:
// new #3 // 创建 File 实例, 引用入栈 (假设 #3 是 File Class Info)
// dup
// ldc #4 // 加载 "kele" (假设 #4 是 String "kele")
// invokespecial #5 // 调用 File.<init>(Ljava/lang/String;)V (消耗引用和 String)
// astore_2 // 将另一个引用存入 file (Slot 2)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
注意
dup
指令在 new
之后、invokespecial
之前的使用,这是标准的对象创建模式。
代码示例 2: 创建数组 (newarray
, anewarray
, multianewarray
)
// 文件名: NewArrayTest.java
public class NewArrayTest {
public void newArray() {
// 1. int[]
int[] intArray = new int[10];
// 字节码:
// bipush 10 // 长度 10 入栈
// newarray 10 // 创建 int 数组 (atype=10), 引用入栈
// astore_1 // 存入 intArray
// 2. Object[]
Object[] objArray = new Object[10];
// 字节码:
// bipush 10 // 长度 10 入栈
// anewarray #6 // 创建 Object 数组 (假设 #6 是 Object Class Info), 引用入栈
// astore_2 // 存入 objArray
// 3. int[][] (二维)
int[][] mintArray = new int[10][10];
// 字节码:
// bipush 10 // 第一维长度 10 入栈
// bipush 10 // 第二维长度 10 入栈
// multianewarray #7, 2 // 创建 int[10][10] (假设 #7 是 [[I), 维度为 2
// astore_3 // 存入 mintArray
// 4. String[][] (只指定第一维)
String[][] strArray = new String[10][];
// 字节码:
// bipush 10 // 长度 10 入栈
// anewarray #8 // 创建 String[] 数组 (假设 #8 是 String Class Info), 注意这里创建的是 String[]
// astore 4 // 存入 strArray (实际上存的是 String[] 的引用)
// 这里创建的是一个包含 10 个 null 的 String[] 数组
}
}
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
# 5.2 字段访问指令
用于读取或写入对象的实例字段或类的静态字段。
- 访问静态字段 (类变量):
getstatic index
: 获取指定类的静态字段的值,并将其压入操作数栈。index
指向常量池CONSTANT_Fieldref_info
。putstatic index
: 将操作数栈顶的值赋给指定类的静态字段。index
指向常量池CONSTANT_Fieldref_info
。
- 访问实例字段:
getfield index
: 获取指定对象的实例字段的值,并将其压入操作数栈。执行前,栈顶需要有对象引用。执行后,对象引用弹出,字段值入栈。index
指向常量池CONSTANT_Fieldref_info
。putfield index
: 将操作数栈顶的值赋给指定对象的实例字段。执行前,栈顶需要依次有value
和objectref
。执行后,value
和objectref
都弹出。index
指向常量池CONSTANT_Fieldref_info
。
代码示例 1: getstatic
// 文件名: GetStaticTest.java
public class GetStaticTest {
public void sayHello() {
// 调用 System.out.println("Hello");
System.out.println("Hello");
// 字节码:
// getstatic #8 // 获取 System.out 静态字段 (PrintStream 引用) 压栈 (假设 #8 指向 System.out)
// ldc #9 // 加载 "Hello" 压栈 (假设 #9 是 String "Hello")
// invokevirtual #10 // 调用 PrintStream.println(String)
// return
}
}
2
3
4
5
6
7
8
9
10
11
12
图示过程:
getstatic
将System.out
(一个PrintStream
对象引用) 压栈。ldc
将字符串"Hello"
的引用压栈。invokevirtual
调用println
方法。它会消耗栈顶的两个元素:参数 ("Hello"
) 和调用对象 (System.out
)。
代码示例 2: getfield
, putfield
, putstatic
// 文件名: FieldAccessTest.java
class Order {
int id; // 实例字段
static String name; // 静态字段
}
public class FieldAccessTest {
public void setOrderId() {
Order order = new Order(); // new, dup, invokespecial, astore_1
// order.id = 1001; (实例字段赋值)
// aload_1 // 加载 order 引用
// sipush 1001 // 加载 1001
// putfield #field_id // 将 1001 赋给 order.id (假设 #field_id 指向 Order.id)
order.id = 1001;
// System.out.println(order.id); (实例字段读取)
// getstatic #system_out // 加载 System.out
// aload_1 // 加载 order 引用
// getfield #field_id // 获取 order.id 的值压栈
// invokevirtual #println_int // 调用 println(int)
System.out.println(order.id);
// Order.name = "ORDER"; (静态字段赋值)
// ldc #string_order // 加载 "ORDER" (假设 #string_order 指向 "ORDER")
// putstatic #field_name // 将 "ORDER" 赋给 Order.name (假设 #field_name 指向 Order.name)
Order.name = "ORDER";
// System.out.println(Order.name); (静态字段读取)
// getstatic #system_out // 加载 System.out
// getstatic #field_name // 获取 Order.name 的值压栈
// invokevirtual #println_string // 调用 println(String)
System.out.println(Order.name); // 注意: 实例引用 order 也能访问静态字段, 但字节码层面是 getstatic
}
}
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
# 5.3 数组操作指令
- 加载数组元素到操作数栈 (
xaload
):baload
(byte/boolean),caload
(char),saload
(short),iaload
(int),laload
(long),faload
(float),daload
(double),aaload
(reference)。- 执行前栈结构:
index
,arrayref
。 - 执行后:
index
和arrayref
弹出,arrayref[index]
的值压栈。
- 将操作数栈顶元素存储到数组 (
xastore
):bastore
(byte/boolean),castore
(char),sastore
(short),iastore
(int),lastore
(long),fastore
(float),dastore
(double),aastore
(reference)。- 执行前栈结构:
value
,index
,arrayref
。 - 执行后: 三个元素弹出,
arrayref[index] = value
。
- 获取数组长度 (
arraylength
):- 执行前栈结构:
arrayref
。 - 执行后:
arrayref
弹出,数组长度 (int) 压栈。
- 执行前栈结构:
代码示例 1: 数组读写 (iaload
, iastore
)
// 文件名: ArrayAccessTest.java
public class ArrayAccessTest {
public void setArray() {
int[] intArray = new int[10]; // bipush 10, newarray 10, astore_1
// intArray[3] = 20;
// aload_1 // 加载 intArray 引用
// iconst_3 // 加载索引 3
// bipush 20 // 加载值 20
// iastore // intArray[3] = 20
intArray[3] = 20;
// System.out.println(intArray[1]);
// getstatic #... // 加载 System.out
// aload_1 // 加载 intArray 引用
// iconst_1 // 加载索引 1
// iaload // 加载 intArray[1] 的值压栈
// invokevirtual #... // 调用 println(int)
System.out.println(intArray[1]);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
代码示例 2: 获取数组长度 (arraylength
)
// 文件名: ArrayLengthTest.java
public class ArrayLengthTest {
public void arrLength(){
double[] arr = new double[10]; // bipush 10, newarray 11 (double), astore_1
// System.out.println(arr.length);
// getstatic #... // 加载 System.out
// aload_1 // 加载 arr 引用
// arraylength // 获取数组长度压栈 (10)
// invokevirtual #... // 调用 println(int)
System.out.println(arr.length);
}
}
2
3
4
5
6
7
8
9
10
11
12
# 5.4 类型检查指令
instanceof index
: 判断栈顶的对象引用 (objectref
) 是否是index
指向的类或其子类的实例,或者是该接口的实现类实例。index
: 指向常量池CONSTANT_Class_info
。- 执行过程: 弹出
objectref
,将判断结果 (1
for true,0
for false) 压入栈。 - 如果
objectref
为null
,结果总是0
。
checkcast index
: 检查栈顶的对象引用 (objectref
) 是否可以强制转换为index
指向的类型。index
: 指向常量池CONSTANT_Class_info
。- 执行过程:
- 如果可以转换(是该类或其子类实例,或是接口实现类实例),则不改变操作数栈(
objectref
仍然在栈顶)。 - 如果
objectref
为null
,可以转换为任何类型,不改变操作数栈。 - 如果不能转换,抛出
ClassCastException
。
- 如果可以转换(是该类或其子类实例,或是接口实现类实例),则不改变操作数栈(
代码示例: instanceof
和 checkcast
// 文件名: TypeCheckTest.java
public class TypeCheckTest {
// 参数 obj 在 Slot 1
public String checkCast(Object obj) {
// if (obj instanceof String)
// aload_1 // 加载 obj
// instanceof #2 // 检查 obj 是否是 String (假设 #2 是 String Class Info)
// ifeq 11 // 如果结果为 0 (false), 跳转到 else 分支
if (obj instanceof String) {
// return (String) obj;
// aload_1 // 加载 obj
// checkcast #2 // 检查 obj 是否能转为 String (如果 instanceof 为 true, 这里总能成功)
// areturn // 返回 obj 引用
return (String) obj;
} else {
// return null;
// aconst_null // 加载 null
// areturn // 返回 null
return null;
}
// 11: (else 分支的开始)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 6. 方法调用与返回指令
# 6.1 方法调用指令
JVM 提供了 5 条指令用于调用方法:
invokevirtual index
: 调用对象的实例方法 (非private
, 非static
, 非final
, 非构造器)。index
: 指向常量池CONSTANT_Methodref_info
。- 动态分派 (Dynamic Dispatch): 在运行时根据对象的实际类型来确定调用哪个方法。这是实现 Java 多态的关键指令。
- 执行过程: 从操作数栈弹出
objectref
(调用对象) 和所有方法参数,然后执行查找和调用。 - 最常见的方法调用方式。
invokeinterface index count
: 调用接口方法。index
: 指向常量池CONSTANT_InterfaceMethodref_info
。count
: (u1) 早期用于性能优化,现在通常不直接使用,但必须存在。- 动态分派: 在运行时搜索实现该接口的对象,找到对应的方法进行调用。
invokespecial index
: 调用需要特殊处理的实例方法。index
: 指向常量池CONSTANT_Methodref_info
。- 静态分派 (Static Dispatch): 调用的目标方法在编译期就已确定,不进行动态查找。
- 主要用于三种情况:
- 实例初始化方法 (
<init>
): 调用构造器。 - 私有方法 (
private
methods)。 - 父类方法 (
super.method()
)。
- 实例初始化方法 (
invokestatic index
: 调用类(静态)方法 (static
methods)。index
: 指向常量池CONSTANT_Methodref_info
(也可以是CONSTANT_InterfaceMethodref_info
,用于调用接口的静态方法)。- 静态分派: 编译期确定目标方法。
invokedynamic index
: (JDK 7+) 调用动态链接的方法。index
: 指向常量池CONSTANT_InvokeDynamic_info
。- 动态语言支持核心: 调用目标在运行时通过引导方法 (Bootstrap Method) 来解析。常用于实现 Lambda 表达式、
invokedynamic
API 等。分派逻辑由用户代码(引导方法)决定。
分派总结:
- 静态分派: 编译期可知,目标唯一。(
invokespecial
,invokestatic
) - 动态分派: 运行时根据对象实际类型决定。(
invokevirtual
,invokeinterface
) - 动态语言分派: 运行时由引导方法决定。(
invokedynamic
)
代码示例 1: invokespecial
// 文件名: InvokeSpecialTest.java
import java.util.Date;
public class InvokeSpecialTest extends Date { // 假设继承自 Date
// 构造器
public InvokeSpecialTest() {
super(); // 隐式调用父类构造器, 使用 invokespecial
}
// 实例方法
public void invoke1(){
// 1. 调用本类构造器 (如果 new InvokeSpecialTest()) - invokespecial
// 2. 调用父类方法
super.toString(); // invokespecial
// 3. 调用私有方法
methodPrivate(); // invokespecial
}
// 私有方法
private void methodPrivate(){
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
代码示例 2: invokestatic
// 文件名: InvokeStaticTest.java
public class InvokeStaticTest {
public void invoke2(){
// 调用静态方法
methodStatic(); // invokestatic
}
public static void methodStatic(){
}
}
2
3
4
5
6
7
8
9
代码示例 3: invokeinterface
// 文件名: InvokeInterfaceTest.java
import java.util.Comparator;
public class InvokeInterfaceTest {
public void invoke3(){
// 调用 Runnable 接口的 run 方法
Thread t1 = new Thread();
((Runnable)t1).run(); // invokeinterface
// 调用 Comparable 接口的 compareTo 方法
Comparable<Integer> com = null;
com.compareTo(123); // invokeinterface
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
代码示例 4: invokevirtual
// 文件名: InvokeVirtualTest.java
public class InvokeVirtualTest {
public void invoke4(){
// 调用 PrintStream 的实例方法 println
System.out.println("hello"); // getstatic System.out, ldc "hello", invokevirtual
// 调用 Thread 的实例方法 run (即使 t1 为 null, 编译时仍生成 invokevirtual, 运行时 NPE)
Thread t1 = null;
t1.run(); // aload_1, invokevirtual
}
}
2
3
4
5
6
7
8
9
10
11
代码示例 5: 接口中的 static
和 default
方法
// 文件名: InterfaceMethodTest.java
interface AA{
// 接口静态方法 (JDK 8+)
public static void method1(){ }
// 接口默认方法 (JDK 8+)
public default void method2(){ }
}
class BB implements AA{ }
public class InterfaceMethodTest {
public static void main(String[] args) {
AA aa = new BB();
// 调用默认方法 method2() -> 通过实例调用
aa.method2(); // invokeinterface AA.method2:()V
// 调用接口静态方法 method1() -> 通过接口名调用
AA.method1(); // invokestatic AA.method1:()V
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
默认方法通过
invokeinterface
调用,静态方法通过 invokestatic
调用。
# 6.2 方法返回指令
用于将控制权从当前方法返回给调用者。
- 根据返回值类型区分:
return
: 用于void
方法、实例初始化方法 (<init>
)、类初始化方法 (<clinit>
)。ireturn
: 返回int
,boolean
,byte
,char
,short
类型的值。lreturn
: 返回long
。freturn
: 返回float
。dreturn
: 返回double
。areturn
: 返回reference
(对象或数组引用)。
- 执行过程:
- 从当前方法的操作数栈顶弹出返回值 (除了
return
)。 - 将返回值压入调用者方法的操作数栈。
- 如果当前方法是
synchronized
的,隐式执行monitorexit
释放锁。 - 销毁当前方法的栈帧。
- 恢复调用者的栈帧,并将 PC 设置为调用指令的下一条指令。
- 从当前方法的操作数栈顶弹出返回值 (除了
代码示例:
// 文件名: ReturnTest.java
public class ReturnTest {
public int returnInt() { return 500; } // sipush 500, ireturn
public double returnDouble() { return 0.0; } // dconst_0, dreturn
public String returnString() { return "hello"; } // ldc "hello", areturn
public int[] returnArr() { return null; } // aconst_null, areturn
public float returnFloat() { int i=10; return i; }// bipush 10, istore_1, iload_1, i2f, freturn
public byte returnByte() { return 0; } // iconst_0, ireturn
public void methodReturn() { /*...*/ } // return
}
2
3
4
5
6
7
8
9
10
# 7. 操作数栈管理指令
用于直接操纵操作数栈,进行如弹出、复制、交换等操作。这些指令不区分数据类型。
- 弹出栈顶元素:
pop
: 弹出栈顶 1 个 Slot 的元素 (如int
,float
,reference
)。pop2
: 弹出栈顶 2 个 Slot 的元素 (如long
,double
,或两个单 Slot 元素)。
- 复制栈顶元素:
dup
: 复制栈顶 1 个 Slot 的元素,并将复制后的值压入栈顶。栈:..., value
->..., value, value
。dup2
: 复制栈顶 2 个 Slot 的元素 (形式 1: 一个long
/double
;形式 2: 两个单 Slot 值),并将复制后的值压入栈顶。- 形式 1:
..., value_word1, value_word2
->..., value_word1, value_word2, value_word1, value_word2
- 形式 2:
..., value2, value1
->..., value2, value1, value2, value1
- 形式 1:
dup_x1
: 复制栈顶 1 个 Slot (value1
),插入到栈顶第二个 Slot (value2
) 之下。栈:..., value2, value1
->..., value1, value2, value1
。dup_x2
: 复制栈顶 1 个 Slot (value1
),插入到栈顶第三个 Slot (value3
/value2
) 之下。- 形式 1 (value2, value3 是单 Slot):
..., value3, value2, value1
->..., value1, value3, value2, value1
- 形式 2 (value2 是双 Slot):
..., value2_word1, value2_word2, value1
->..., value1, value2_word1, value2_word2, value1
- 形式 1 (value2, value3 是单 Slot):
dup2_x1
: 复制栈顶 2 个 Slot (形式 1 或 2),插入到其下的 1 个 Slot (value3
) 之下。- 形式 1 (复制 long/double):
..., value3, value2_word1, value2_word2
->..., value2_word1, value2_word2, value3, value2_word1, value2_word2
- 形式 2 (复制两个单 Slot):
..., value3, value2, value1
->..., value2, value1, value3, value2, value1
- 形式 1 (复制 long/double):
dup2_x2
: 复制栈顶 2 个 Slot (形式 1 或 2),插入到其下的 2 个 Slot 之下。
- 交换栈顶元素:
swap
: 交换栈顶 2 个单 Slot 元素的位置。栈:..., value2, value1
->..., value1, value2
。 (不支持long
/double
)
- 无操作:
nop
: (Opcode 0x00) 不执行任何操作。可用于调试、占位或对齐。
代码示例 1: pop
(方法返回值未使用)
// 文件名: PopTest1.java
public class PopTest1 {
public void print() {
Object obj = new Object(); // new, dup, invokespecial, astore_1
// 调用 obj.toString() 但不使用其返回值 String
obj.toString();
// aload_1 (加载 obj)
// invokevirtual #... toString() // 返回 String 引用压栈
// pop // 返回的 String 引用未使用,弹出栈顶 (1 Slot)
}
// 如果接收返回值:
// String info = obj.toString();
// aload_1
// invokevirtual #... toString()
// astore_2 // 将返回的 String 引用存入局部变量 info,不使用 pop
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
代码示例 2: pop2
(long/double 返回值未使用)
// 文件名: PopTest2.java
public class PopTest2 {
public void foo(){
// 调用 bar() 但不使用其返回值 long
bar();
// invokespecial #... bar() // 返回 long 压栈 (占 2 Slot)
// pop2 // 返回的 long 未使用,弹出栈顶 (2 Slot)
}
public long bar(){
return 0L; // lconst_0, lreturn
}
}
2
3
4
5
6
7
8
9
10
11
12
代码示例 3: dup
, putfield
中的 pop2
// 文件名: DupPopTest.java
public class DupPopTest {
private long index = 0L; // 实例字段
public long nextIndex() {
// return index++; (先返回旧值,index 再自增)
// 1. 加载 this
// aload_0
// 2. 复制 this 引用 (一个给 getfield, 一个给 putfield)
// dup
// 3. 获取旧的 index 值压栈 (long, 占 2 Slot)
// getfield #index
// 4. 再次复制旧的 index 值 (一个用于返回, 一个用于计算新值) -> 这里是 dup2
// dup2
// 5. 将旧值存起来稍后返回 (需要临时变量或复杂栈操作,但 javap 显示的不一定如此直接)
// 6. 加载常量 1L
// lconst_1
// 7. 计算新值 (旧值 + 1L)
// ladd
// 8. 将新值存回字段 index (消耗 this 引用和新值)
// putfield #index
// 9. 返回旧值 (之前复制的那个)
// lreturn
// javap 的简化表示可能如下:
// aload_0 (this)
// dup (this, this)
// getfield #index (this, old_val_word1, old_val_word2)
// dup2 (this, old_val_w1, old_val_w2, old_val_w1, old_val_w2)
// lconst_1 (this, old_val_w1, old_val_w2, old_val_w1, old_val_w2, 1L_w1, 1L_w2)
// ladd (this, old_val_w1, old_val_w2, new_val_w1, new_val_w2)
// putfield #index (old_val_w1, old_val_w2) <-- putfield 消耗 this 和 new_val
// lreturn (return old_val)
return index++;
}
}
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
(这个例子的字节码和栈变化比较复杂,
dup2
用于同时复制 long
值的两个 Slot。 putfield
会消耗掉对象引用和要赋的值,这里是 this
和计算后的新 index
值)
# 8. 控制转移指令
用于改变程序执行流程,实现分支、循环、跳转等。
# 8.1 比较指令 (lcmp
, fcmp*
, dcmp*
)
- 已在算术指令部分介绍 (3.6 节)。
- 它们比较栈顶两个
long
,float
或double
值,并将结果-1
,0
, 或1
压入栈。 - 是后续条件跳转指令的基础。
# 8.2 条件跳转指令 (if*
)
根据栈顶单个 int 值的条件进行跳转。
- 指令:
ifeq offset
: 如果栈顶 int 值 等于 0,跳转。ifne offset
: 如果栈顶 int 值 不等于 0,跳转。iflt offset
: 如果栈顶 int 值 小于 0,跳转。ifle offset
: 如果栈顶 int 值 小于等于 0,跳转。ifgt offset
: 如果栈顶 int 值 大于 0,跳转。ifge offset
: 如果栈顶 int 值 大于等于 0,跳转。ifnull offset
: 如果栈顶引用值为null
,跳转。ifnonnull offset
: 如果栈顶引用值不为null
,跳转。
- 操作数
offset
: 2 字节带符号整数,表示相对于当前指令地址的跳转偏移量。 - 执行过程: 弹出栈顶元素,检查条件。若满足,PC 跳转到
current_pc + offset
;否则,继续执行下一条指令。 - 类型处理:
boolean
,byte
,char
,short
的比较结果直接使用这些if*
指令判断。long
,float
,double
需要先用lcmp
/fcmp*
/dcmp*
指令得到int
结果 (-1, 0, 1),再用ifeq
,iflt
等指令判断这个结果。
代码示例 1: ifeq
// 文件名: IfEqTest.java
public class IfEqTest {
public void compare1(){
int a = 0; // iconst_0, istore_1
// if(a == 0) { a = 20; } else { a = 10; }
// iload_1 (加载 a=0)
// ifeq 12 (如果 a == 0, 跳转到 12) -> 跳转发生
if(a != 0){ // 源码是 != 0, 但字节码通常优化为 == 0 跳转到 else
a = 10; // (这部分代码被跳过)
} else {
// 12: bipush 20
// 14: istore_1 (a = 20)
a = 20;
}
// 15: return
}
}
// 对应字节码片段 (注意与源码逻辑的 slight difference in branching)
// 2: iload_1
// 3: ifeq 12 // 如果 a == 0 跳转到 12 (else 分支)
// 6: bipush 10 // (if 分支, a=10)
// 8: istore_1
// 9: goto 15 // 跳过 else
// 12: bipush 20 // (else 分支, a=20)
// 14: istore_1
// 15: return
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
代码示例 2: ifnonnull
// 文件名: IfNullTest.java
public class IfNullTest {
// 参数 str 在 Slot 1
public boolean compareNull(String str){
// if(str != null) { return false; } else { return true; }
// aload_1 (加载 str)
// ifnonnull 6 (如果 str != null, 跳转到 6)
if(str == null){ // 源码是 == null
// (如果 nonnull 不跳转, 说明 str == null)
// iconst_1
// ireturn (返回 true)
return true;
} else {
// 6: iconst_0
// 7: ireturn (返回 false)
return false;
}
}
}
// 对应字节码
// 0: aload_1
// 1: ifnonnull 6 // 如果 str != null 跳转到 6 (else 分支)
// 4: iconst_1 // (if 分支, 返回 true)
// 5: ireturn
// 6: iconst_0 // (else 分支, 返回 false)
// 7: ireturn
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
代码示例 3: float
比较 + ifge
// 文件名: FloatCompareTest.java
public class FloatCompareTest {
public void compare2() {
float f1 = 9f; // ldc, fstore_1
float f2 = 10f; // ldc, fstore_2
// System.out.println(f1 < f2); // 期望 true
// getstatic #...
// fload_1
// fload_2
// fcmpg (f1 < f2, 结果 -1 压栈)
// ifge 19 (如果 -1 >= 0, 跳转) -> 不跳转
// iconst_1 (压入 1, 代表 true)
// goto 20
// 19: iconst_0 (压入 0, 代表 false)
// 20: invokevirtual #... println(Z)
System.out.println(f1 < f2);
}
}
// 对应字节码
// 11: fcmpg // 比较 f1 和 f2, 结果 -1 入栈
// 12: ifge 19 // 如果 >= 0 跳转 (条件不满足)
// 15: iconst_1
// 16: goto 20
// 19: iconst_0
// 20: invokevirtual #5 // 打印 boolean
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
代码示例 4: long
比较 + ifle
// 文件名: LongCompareTest.java
public class LongCompareTest {
public void compare3() {
int i1 = 10; // bipush, istore_1
long l1 = 20L; // ldc2_w, lstore_2
// System.out.println(i1 > l1); // 期望 false
// getstatic #...
// iload_1
// i2l (将 i1 转为 long)
// lload_2
// lcmp (10L < 20L, 结果 -1 压栈)
// ifle 21 (如果 -1 <= 0, 跳转) -> 跳转
// iconst_1 (被跳过)
// goto 22
// 21: iconst_0 (压入 0, 代表 false)
// 22: invokevirtual #... println(Z)
System.out.println(i1 > l1);
}
}
// 对应字节码
// 13: lcmp // 比较 long, 结果 -1 入栈
// 14: ifle 21 // 如果 <= 0 跳转 (条件满足)
// 17: iconst_1
// 18: goto 22
// 21: iconst_0
// 22: invokevirtual #5 // 打印 boolean
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
代码示例 5: double
比较 + ifle
// 文件名: DoubleCompareTest.java
public class DoubleCompareTest {
// 参数 d 在 Slot 1, 2
public int compare4(double d) {
// if (d > 50.0) { return 1; } else { return -1; }
// dload_1 (加载 d)
// ldc2_w #... (加载 50.0)
// dcmpl (比较 d 和 50.0)
// ifle 10 (如果 d <= 50.0, 跳转到 else 分支)
if (d > 50.0) {
// (如果 dcmpl 结果 > 0, ifle 不跳转)
// iconst_1
// ireturn (返回 1)
return 1;
} else {
// 10: iconst_m1 (-1)
// 11: ireturn (返回 -1)
return -1;
}
}
}
// 对应字节码
// 4: dcmpl // 比较 d 和 50.0
// 5: ifle 10 // 如果 d <= 50.0 跳转到 10 (else 分支)
// 8: iconst_1 // (if 分支)
// 9: ireturn
// 10: iconst_m1 // (else 分支)
// 11: ireturn
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
# 8.3 比较条件跳转指令 (if_icmp*
, if_acmp*
)
将比较栈顶两个元素和条件跳转合并为一条指令。
- 整数比较跳转 (
if_icmp*
):if_icmpeq offset
: 如果栈顶两个 int 相等,跳转。if_icmpne offset
: 如果栈顶两个 int 不相等,跳转。if_icmplt offset
: 如果次顶 < 栈顶,跳转。if_icmple offset
: 如果次顶 <= 栈顶,跳转。if_icmpgt offset
: 如果次顶 > 栈顶,跳转。if_icmpge offset
: 如果次顶 >= 栈顶,跳转。
- 引用比较跳转 (
if_acmp*
):if_acmpeq offset
: 如果栈顶两个引用相等 (指向同一对象或都为 null),跳转。if_acmpne offset
: 如果栈顶两个引用不相等,跳转。
- 执行过程: 从操作数栈弹出两个元素进行比较。若满足条件,则跳转;否则继续。比较后,两个元素都被消耗。
代码示例 1: if_icmple
// 文件名: IfIcmpTest1.java
public class IfIcmpTest1 {
public void ifCompare1(){
int i = 10; // bipush, istore_1
int j = 20; // bipush, istore_2
// System.out.println(i > j); // 期望 false
// getstatic #...
// iload_1 (压入 i=10)
// iload_2 (压入 j=20)
// if_icmple 18 (比较 i 和 j, 如果 i <= j, 跳转) -> 跳转
// iconst_1 (被跳过)
// goto 19
// 18: iconst_0 (压入 0)
// 19: invokevirtual #... println(Z)
System.out.println(i > j);
}
}
// 对应字节码
// 9: iload_1
// 10: iload_2
// 11: if_icmple 18 // 比较 i, j, 如果 i <= j 跳转 (满足)
// 14: iconst_1
// 15: goto 19
// 18: iconst_0
// 19: invokevirtual #5 // 打印 boolean
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
代码示例 2: short
/byte
比较 (隐式转 int)
// 文件名: IfIcmpTest2.java
public class IfIcmpTest2 {
public void ifCompare2() {
short s1 = 9; // bipush, istore_1
byte b1 = 10; // bipush, istore_2
// System.out.println(s1 > b1); // 期望 false
// ...
// iload_1 (加载 s1, 已是 int)
// iload_2 (加载 b1, 已是 int)
// if_icmple 18 (比较 9 <= 10, 跳转) -> 跳转
// ... (同上)
System.out.println(s1 > b1);
}
}
// 字节码与示例 1 非常相似
2
3
4
5
6
7
8
9
10
11
12
13
14
15
代码示例 3: if_acmpne
, if_acmpeq
// 文件名: IfAcmpTest.java
public class IfAcmpTest {
public void ifCompare3() {
Object obj1 = new Object(); // new... astore_1
Object obj2 = new Object(); // new... astore_2
// System.out.println(obj1 == obj2); // 期望 false
// getstatic #...
// aload_1
// aload_2
// if_acmpne 28 (比较 obj1, obj2 引用, 不相等则跳转) -> 跳转
// iconst_1 (被跳过)
// goto 29
// 28: iconst_0
// 29: invokevirtual #... println(Z)
System.out.println(obj1 == obj2);
// System.out.println(obj1 != obj2); // 期望 true
// getstatic #...
// aload_1
// aload_2
// if_acmpeq 44 (比较 obj1, obj2 引用, 相等则跳转) -> 不跳转
// iconst_1
// goto 45
// 44: iconst_0 (被跳过)
// 45: invokevirtual #... println(Z)
System.out.println(obj1 != obj2);
}
}
// 对应字节码
// 19: aload_1
// 20: aload_2
// 21: if_acmpne 28 // 不相等则跳转 (满足)
// ...
// 35: aload_1
// 36: aload_2
// 37: if_acmpeq 44 // 相等则跳转 (不满足)
// ...
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
# 8.4 多条件分支跳转 (tableswitch
, lookupswitch
)
用于实现 switch-case
语句。
tableswitch
: 用于case
值是连续或近似连续的情况。- 结构:
tableswitch <default_offset> <low_value> <high_value> <jump_offset_for_low> ... <jump_offset_for_high>
- 执行: 从操作数栈弹出
index
(int)。- 如果
index
在[low, high]
范围内,则跳转到base_pc + jump_offset_for_index
。 - 否则,跳转到
base_pc + default_offset
。
- 如果
- 效率高: 通过索引直接计算偏移量,O(1) 时间复杂度。
- 结构:
lookupswitch
: 用于case
值是离散或稀疏的情况。- 结构:
lookupswitch <default_offset> <n_pairs> <match1>:<offset1> <match2>:<offset2> ... <matchN>:<offsetN>
(pairs 按 match 值排序) - 执行: 从操作数栈弹出
key
(int)。- 在
match-offset
对中搜索等于key
的match
。 - 如果找到,跳转到
base_pc + offset
。 - 如果找不到,跳转到
base_pc + default_offset
。
- 在
- 效率较低: 需要进行搜索,时间复杂度 O(logN) 或 O(N),取决于实现。
- 结构:
编译器选择: 编译器会根据 case
值的分布情况自动选择使用 tableswitch
(如果 case 值密集) 或 lookupswitch
(如果 case 值稀疏)。
代码示例 1: tableswitch
(case 1, 2, 3)
// 文件名: TableSwitchTest.java
public class TableSwitchTest {
// 参数 select 在 Slot 1
public void swtich1(int select){
int num; // Slot 2
switch(select){
case 1: num = 10; break; // 跳转 28
case 2: num = 20; // 跳转 34 (无 break, 会贯穿)
case 3: num = 30; break; // 跳转 37 (case 2 会执行到这里)
default: num = 40; // 跳转 43
}
// break 对应 goto 指令跳出 switch
}
}
// 对应字节码
// 0: iload_1
// 1: tableswitch { // 1 to 3
// 1: 28; // case 1 跳转到 PC=28
// 2: 34; // case 2 跳转到 PC=34
// 3: 37; // case 3 跳转到 PC=37
// default: 43 // default 跳转到 PC=43
// }
// 28: bipush 10
// 30: istore_2 // num = 10
// 31: goto 46 // break (跳到 return)
// 34: bipush 20
// 36: istore_2 // num = 20 (无 break)
// 37: bipush 30
// 39: istore_2 // num = 30
// 40: goto 46 // break
// 43: bipush 40
// 45: istore_2 // num = 40
// 46: return
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
代码示例 2: lookupswitch
(case 100, 500, 200)
// 文件名: LookupSwitchTest.java
public class LookupSwitchTest {
// 参数 select 在 Slot 1
public void swtich2(int select){
int num; // Slot 2
switch(select){
case 100: num = 10; break; // 跳转 36
case 500: num = 20; break; // 跳转 42
case 200: num = 30; break; // 跳转 48
default: num = 40; // 跳转 54
}
}
}
// 对应字节码 (注意 case 顺序可能被重排)
// 0: iload_1
// 1: lookupswitch { // 3 pairs
// 100: 36; // key 100 跳转到 PC=36
// 200: 48; // key 200 跳转到 PC=48
// 500: 42; // key 500 跳转到 PC=42
// default: 54 // default 跳转到 PC=54
// }
// 36: bipush 10
// 38: istore_2 // num = 10
// 39: goto 57 // break
// 42: bipush 20
// 44: istore_2 // num = 20
// 45: goto 57 // break
// 48: bipush 30
// 50: istore_2 // num = 30
// 51: goto 57 // break
// 54: bipush 40
// 56: istore_2 // num = 40
// 57: return
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
代码示例 3: String
switch (JDK 7+)
// 文件名: StringSwitchTest.java
public class StringSwitchTest {
// 参数 season 在 Slot 1
public void swtich3(String season){
// Java 7 开始支持 String switch
// 编译器实现通常分两步:
// 1. 使用 String.hashCode() 配合 lookupswitch 跳转到对应 case 的代码块。
// 2. 在每个 case 代码块开头,使用 String.equals() 再次确认,防止哈希碰撞。
// 3. 确认后,执行 case 逻辑或跳转到下一个 switch (tableswitch) 处理 break/fallthrough。
switch(season){
case "SPRING":break;
case "SUMMER":break;
case "AUTUMN":break;
case "WINTER":break;
}
}
}
// 对应字节码 (非常复杂)
// 0: aload_1
// 1: astore_2 // 复制 season 引用
// 2: iconst_m1 // 默认 case 索引 -1
// 3: istore_3
// 4: aload_2
// 5: invokevirtual #hashcode // 计算 season.hashCode()
// 8: lookupswitch { // 根据 hashCode 跳转
// -1842350579: 52; // hashCode("SPRING")
// -1837878353: 66; // hashCode("SUMMER")
// -1734407483: 94; // hashCode("WINTER")
// 1941980694: 80; // hashCode("AUTUMN")
// default: 105
// }
// // --- Case "SPRING" ---
// 52: aload_2
// 53: ldc #spring_const // 加载 "SPRING" 常量
// 55: invokevirtual #equals // 调用 season.equals("SPRING")
// 58: ifeq 105 // 如果不相等, 跳到 switch 结束
// 61: iconst_0 // 如果相等, 设置 case 索引为 0
// 62: istore_3
// 63: goto 105 // 跳到 switch 结束
// // --- Case "SUMMER" --- (类似)
// 66: aload_2
// 67: ldc #summer_const
// ...
// 75: iconst_1
// 76: istore_3
// 77: goto 105
// // --- Case "AUTUMN" --- (类似)
// 80: aload_2
// 81: ldc #autumn_const
// ...
// 89: iconst_2
// 90: istore_3
// 91: goto 105
// // --- Case "WINTER" --- (类似)
// 94: aload_2
// 95: ldc #winter_const
// ...
// 103: iconst_3
// 104: istore_3
// // --- Switch 结束, 根据 case 索引处理 break ---
// 105: iload_3 // 加载最终确定的 case 索引
// 106: tableswitch { // 0 to 3
// 0: 136; // case 0 ("SPRING") 跳转
// 1: 139; // case 1 ("SUMMER") 跳转
// 2: 142; // case 2 ("AUTUMN") 跳转
// 3: 145; // case 3 ("WINTER") 跳转
// default: 145 // default (-1) 或 fallthrough 跳转
// }
// 136: goto 145 // "SPRING" 的 break
// 139: goto 145 // "SUMMER" 的 break
// 142: goto 145 // "AUTUMN" 的 break
// 145: return // "WINTER" 的 break 或 default
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
61
62
63
64
65
66
67
68
69
70
71
72
String switch 的字节码实现比基本类型 switch 复杂得多。
# 8.5 无条件跳转 (goto
, goto_w
)
强制将 PC 跳转到指定位置。
goto offset
: 跳转到current_pc + offset
。offset
是 2 字节带符号整数。goto_w offset
: 功能同goto
,但offset
是 4 字节带符号整数,支持更大的跳转范围。jsr
,ret
: 已废弃,用于早期finally
实现,不推荐使用。
代码示例 1: while
循环 (int
)
// 文件名: WhileIntTest.java
public class WhileIntTest {
public void whileInt() {
int i = 0; // iconst_0, istore_1
// 循环条件检查 (标签 L_LOOP_START):
// 2: iload_1
// 3: bipush 100
// 5: if_icmpge 17 (如果 i >= 100, 跳转到循环结束 L_LOOP_END)
while (i < 100) {
// 循环体:
String s = "youngkbt.cn"; // ldc, astore_2
// i++;
i++; // iinc 1 by 1
// 无条件跳转回循环条件检查
// 14: goto 2 (跳转回 L_LOOP_START)
}
// 循环结束 (标签 L_LOOP_END):
// 17: return
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
代码示例 2: while
循环 (double
)
// 文件名: WhileDoubleTest.java
public class WhileDoubleTest {
public void whileDouble() {
double d = 0.0; // dconst_0, dstore_1
// L_LOOP_START:
// 2: dload_1
// 3: ldc2_w #... (100.1)
// 6: dcmpg (比较 d 和 100.1)
// 7: ifge 20 (如果 d >= 100.1, 跳转到 L_LOOP_END)
while(d < 100.1) {
String s = "youngkbt.cn"; // ldc, astore_3
// d++;
// dload_1
// dconst_1
// dadd
// dstore_1
d++;
// 17: goto 2 (跳转回 L_LOOP_START)
}
// L_LOOP_END:
// 20: return
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
代码示例 3: for
循环
// 文件名: ForTest.java
public class ForTest {
public void printFor() {
// short i; (局部变量声明)
// 初始化: i = 0
// 0: iconst_0
// 1: istore_1 (i 在 Slot 1)
// 循环条件检查 (L_LOOP_COND):
// 2: iload_1
// 3: bipush 100
// 5: if_icmpge 19 (如果 i >= 100, 跳转到 L_LOOP_END)
for (short i = 0; i < 100; i++) { // 注意 i 的类型
// 循环体:
String s = "youngkbt.cn"; // ldc, astore_2
// 更新语句: i++ (注意 short -> int -> short)
// iload_1 (加载 i)
// iconst_1
// iadd (i+1, 结果是 int)
// i2s (将 int 转回 short)
// istore_1 (存回 i)
}
// 无条件跳转回循环条件检查
// 16: goto 2 (跳转回 L_LOOP_COND)
// 循环结束 (L_LOOP_END):
// 19: return
}
}
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
代码练习: while
vs for
vs do-while
字节码对比
// 文件名: LoopCompare.java
public class LoopCompare {
// while: 条件检查在循环体之前
public void whileTest(){
int i = 1; // iconst_1, istore_1
// L_COND:
// iload_1
// bipush 100
// if_icmpgt 14 // 如果 i > 100 跳转 L_END
while(i <= 100){
// L_BODY:
i++; // iinc 1 by 1
// goto L_COND
}
// L_END: return
}
// for: 结构类似 while, 初始化只执行一次
public void forTest(){
// 初始化: int i = 1; (只执行一次)
// iconst_1, istore_1
// goto L_COND // 先跳到条件检查
// L_BODY: (循环体为空)
// L_UPDATE: i++;
// iinc 1 by 1
// L_COND: 条件检查
// iload_1
// bipush 100
// if_icmple 8 // 如果 i <= 100 跳转回 L_BODY (实际跳转到更新 L_UPDATE)
for(int i = 1; i <= 100; i++){
}
// L_END: return
// 注意: for 循环变量 i 的作用域仅限循环内
}
// do-while: 循环体先执行,条件检查在后
public void doWhileTest(){
int i = 1; // iconst_1, istore_1
// L_BODY:
do{
i++; // iinc 1 by 1
// L_COND: 条件检查
// iload_1
// bipush 100
// if_icmple 2 // 如果 i <= 100 跳转回 L_BODY
} while(i <= 100);
// L_END: return
}
}
// 字节码大致结构:
// whileTest: L_COND -> [Body -> goto L_COND] or L_END
// forTest: Init -> goto L_COND -> [Body -> Update -> L_COND] or L_END
// doWhileTest: L_BODY -> L_COND -> [goto L_BODY] or L_END
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
# 9. 异常处理指令
# 9.1 抛出异常指令 (athrow
)
- 作用: 用于显式抛出异常(对应 Java 代码中的
throw
语句)。 - 执行过程:
- 要求操作数栈顶必须是一个异常对象的引用 (
Throwable
或其子类)。 - 弹出该异常对象引用。
- 清空当前方法的操作数栈。
- 查找当前方法的异常表 (Exception Table),看是否有能处理该异常类型的
catch
块。 - 如果找到匹配的处理器,将 PC 设置到处理器的起始地址,并将异常对象引用压入新的(空的)操作数栈。
- 如果未找到匹配的处理器,则弹出当前方法的栈帧,并将异常对象重新抛给调用者方法,重复步骤 4-6。
- 如果一直到虚拟机栈底都没有找到处理器,则线程终止。
- 要求操作数栈顶必须是一个异常对象的引用 (
- 隐式抛出: JVM 在执行某些指令检测到错误时也会自动抛出异常(如
idiv
除零抛ArithmeticException
,checkcast
类型不匹配抛ClassCastException
),其后续处理流程与athrow
类似。
代码示例 1: 显式 throw
// 文件名: ThrowTest1.java
public class ThrowTest1 {
// 参数 i 在 Slot 1
public void throwZero(int i){
// if (i != 0) return;
// iload_1
// ifne 14 // 如果 i != 0, 跳转到 return
if(i == 0){
// (如果 i == 0, ifne 不跳转)
// new #... RuntimeException // 创建异常对象, 引用压栈
// dup // 复制引用 (一个给构造器, 一个给 athrow)
// ldc #... "参数值为0" // 加载错误消息
// invokespecial #... <init>(String) // 调用构造器
// athrow // 弹出异常对象引用, 开始异常处理流程
throw new RuntimeException("参数值为0");
}
// 14: return
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
代码示例 2: throws
声明与 throw
// 文件名: ThrowTest2.java
import java.io.IOException;
public class ThrowTest2 {
// throws 声明只影响编译期检查和方法签名, 不直接生成字节码
// 参数 i 在 Slot 1
public void throwOne(int i) throws RuntimeException, IOException {
// if (i != 1) return;
// iload_1
// iconst_1
// if_icmpne 15 // 如果 i != 1, 跳转到 return
if(i == 1){
// (如果 i == 1, 不跳转)
// new #... RuntimeException
// dup
// ldc #... "参数值为1"
// invokespecial #... <init>(String)
// athrow
throw new RuntimeException("参数值为1");
}
// 15: return
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 9.2 异常处理与异常表 (Exception Table)
- 异常处理机制: Java 的
try-catch-finally
结构不是通过特定的字节码指令实现的,而是依赖于每个方法关联的异常表。 - 异常表:
- 存储在 Class 文件方法表的
Code
属性中。 - 是一个包含多个条目 (
exception_info
) 的表。 - 每个条目定义了一个异常处理器,包含:
start_pc
: 处理器监控的代码块起始字节码偏移量 (包含)。end_pc
: 处理器监控的代码块结束字节码偏移量 (不包含)。handler_pc
: 异常处理代码 (catch
块) 的起始字节码偏移量。catch_type
: 指向常量池CONSTANT_Class_info
的索引,表示该处理器捕获的异常类型。如果为 0,表示捕获任何异常(通常用于finally
)。
- 存储在 Class 文件方法表的
- 工作流程: 当在
[start_pc, end_pc)
范围内抛出异常时,JVM 遍历异常表:- 查找
catch_type
与抛出的异常类型匹配(或为其父类)的条目。 - 如果找到,将 PC 设置为对应条目的
handler_pc
,并将异常对象压栈,开始执行catch
块。 - 如果未找到,或者在
catch
块中又抛出新异常,则重复在调用者方法中查找异常表的过程。
- 查找
finally
的实现: 编译器会复制finally
块的代码:- 在
try
块正常结束(如return
或执行完最后一句)之前插入一份finally
代码。 - 在每个
catch
块结束之前插入一份finally
代码。 - 异常表中会有一个
catch_type
为 0 的条目,其handler_pc
指向一份finally
代码,用于处理try
或catch
块中抛出但未被捕获的异常。该finally
代码执行完后会重新抛出异常 (athrow
)。
- 在
代码示例 1: try-catch
// 文件名: TryCatchTest.java
import java.io.*;
public class TryCatchTest {
public void tryCatch(){
try{ // try 块对应字节码 PC: 0 - 22
File file = new File("d:/hello.txt"); // 0-9
FileInputStream fis = new FileInputStream(file); // 10-18
String info = "hello!"; // 19-21
} catch (FileNotFoundException e) { // catch 块 1 Handler PC: 25
e.printStackTrace(); // 26-27
} catch(RuntimeException e){ // catch 块 2 Handler PC: 33
e.printStackTrace(); // 34-35
}
// 正常结束路径
// 22: goto 38 (跳过 catch 块)
// 异常处理跳转点
// 25: astore_1 (异常对象存入 Slot 1) ... 处理 FileNotFoundException
// 30: goto 38 (处理完后跳到 return)
// 33: astore_1 (异常对象存入 Slot 1) ... 处理 RuntimeException
// ...
// 38: return
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
对应异常表 (javap -v
):
Exception table:
from to target type
0 22 25 Class java/io/FileNotFoundException // try 块 (0-21) 发生 FNFException, 跳转到 25
0 22 33 Class java/lang/RuntimeException // try 块 (0-21) 发生 RuntimeException, 跳转到 33
2
3
4
代码示例 2: try-finally
返回值问题 (面试常考)
// 文件名: FinallyReturnTest.java
public class FinallyReturnTest {
public static String func() {
String str = "hello"; // Slot 0
try {
// 1. 将要返回的值 "hello" 保存到一个临时局部变量 (Slot 1)
// aload_0, astore_1
// 2. 执行 finally 块代码
// ldc "kele", astore_0 (修改了 str, 但不影响临时变量)
// 3. 从临时局部变量加载返回值 "hello"
// aload_1
// 4. 返回 "hello"
// areturn
return str;
} finally {
str = "kele"; // finally 块会修改 str, 但 try 的返回值已暂存
}
}
public static void main(String[] args) {
System.out.println(func()); // 输出 "hello"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
对应字节码 (简化逻辑):
0: ldc #17 // "hello"
2: astore_0 // str = "hello"
// --- try block ---
3: aload_0 // 加载 str ("hello")
4: astore_1 // 将返回值暂存到 Slot 1
// --- finally block (normal path) ---
5: ldc #18 // "kele"
7: astore_0 // str = "kele" (修改 Slot 0)
// --- return from try (using saved value) ---
8: aload_1 // 加载暂存的返回值 ("hello" from Slot 1)
9: areturn // 返回 "hello"
// --- Exception Handler for finally ---
10: astore_2 // 保存异常对象
// --- finally block (exception path) ---
11: ldc #18 // "kele"
13: astore_0 // str = "kele"
// --- rethrow exception ---
14: aload_2 // 加载异常对象
15: athrow // 重新抛出
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
对应异常表:
Exception table:
from to target type
3 5 10 any // try 块 (3-4) 发生任何异常, 跳转到 10 执行 finally 并 rethrow
2
3
关键在于
try
块的 return
会将返回值暂存,finally
对原变量的修改不影响这个暂存值。
# 10. 同步控制指令 (monitorenter
, monitorexit
)
用于支持 Java 的 synchronized
关键字,实现线程同步。
# 10.1 同步机制概述
JVM 支持两种同步方式:
- 方法级同步: 使用
synchronized
修饰方法。 - 代码块同步: 使用
synchronized (object) { ... }
语句块。
两种方式都依赖于对象监视器 (Monitor) 来实现。每个 Java 对象都有一个关联的监视器。
# 10.2 方法级同步的实现
- 隐式实现: 方法级的同步不使用
monitorenter
/monitorexit
字节码指令。 - 访问标志: JVM 通过检查方法表结构中的
ACC_SYNCHRONIZED
访问标志来识别同步方法。 - 执行流程:
- 调用同步方法时,JVM 自动获取该方法所属对象(实例方法)或类对象(静态方法)的监视器锁。
- 执行方法体。
- 方法正常完成或异常终止时,JVM 自动释放锁。
代码示例:
// 文件名: SyncMethodTest.java
public class SyncMethodTest {
private int i = 0;
// 同步实例方法
public synchronized void add() { // 方法表 flags 包含 ACC_SYNCHRONIZED
i++;
}
}
2
3
4
5
6
7
8
add
方法的字节码 (与非同步版本相同):
0: aload_0
1: dup
2: getfield #2 // i
5: iconst_1
6: iadd
7: putfield #2 // i
10: return
2
3
4
5
6
7
字节码本身没有同步指令,同步由 JVM 根据 ACC_SYNCHRONIZED
标志在方法调用和返回时处理。
# 10.3 代码块同步的实现
- 显式指令: 使用
monitorenter
和monitorexit
指令实现。 monitorenter
:- 需要操作数栈顶有一个对象引用 (
objectref
),作为要锁定的对象。 - 尝试获取
objectref
关联监视器的锁。 - 如果获取成功(锁计数器为 0,或当前线程已持有该锁),则将锁计数器加 1,弹出
objectref
。 - 如果获取失败,线程阻塞等待。
- 需要操作数栈顶有一个对象引用 (
monitorexit
:- 需要操作数栈顶有一个对象引用 (
objectref
),必须是之前monitorenter
锁定的同一个对象。 - 将
objectref
关联监视器的锁计数器减 1。 - 如果计数器变为 0,则释放锁,唤醒等待该锁的线程。
- 弹出
objectref
。
- 需要操作数栈顶有一个对象引用 (
- 编译器保证: 编译器必须确保每个
monitorenter
都有至少一个对应的monitorexit
在所有可能的执行路径上(正常或异常)被执行。- 正常路径: 在同步代码块末尾插入
monitorexit
。 - 异常路径: 编译器自动生成一个隐藏的异常处理器 (catch-any),其处理逻辑是执行
monitorexit
释放锁,然后重新抛出异常 (athrow
)。
- 正常路径: 在同步代码块末尾插入
代码示例:
// 文件名: SyncBlockTest.java
public class SyncBlockTest {
private Object obj = new Object(); // Slot 1: obj
private int i = 0; // Slot 2: i
public void subtract(){
// synchronized (obj) { ... }
// 1. 获取锁对象 obj 引用压栈
// aload_0
// getfield #obj
// 2. 复制一份引用 (给 monitorenter 和 monitorexit 用)
// dup
// 3. 将复制的引用存入临时局部变量 (例如 Slot 1)
// astore_1
// 4. 进入同步块,获取锁
// monitorenter (消耗栈顶的 obj 引用)
synchronized (obj){
// --- 同步代码块 ---
// aload_0
// dup
// getfield #i
// iconst_1
// isub
// putfield #i
i--;
// --- 正常退出同步块,释放锁 ---
// aload_1 (加载之前保存的 obj 引用)
// monitorexit
} // 编译器在此处插入 monitorexit (正常路径)
// goto L_AFTER_SYNC // 跳过异常处理代码
// --- 异常处理程序 (由编译器生成) ---
// L_EXCEPTION_HANDLER:
// astore_2 // 保存异常对象
// aload_1 (加载之前保存的 obj 引用)
// monitorexit // 异常路径释放锁
// aload_2 (加载异常对象)
// athrow // 重新抛出异常
// L_AFTER_SYNC:
// return
}
}
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
对应字节码:
0: aload_0
1: getfield #4 // obj
4: dup
5: astore_1 // 将 obj 存入 Slot 1 (用于后续 monitorexit)
6: monitorenter // 获取 obj 的锁
// --- try block (同步代码块) ---
7: aload_0
8: dup
9: getfield #2 // i
12: iconst_1
13: isub
14: putfield #2 // i
17: aload_1 // 加载 obj (来自 Slot 1)
18: monitorexit // 正常退出,释放锁
19: goto 27 // 跳转到 return
// --- exception handler (catch-any) ---
22: astore_2 // 保存异常对象
23: aload_1 // 加载 obj (来自 Slot 1)
24: monitorexit // 异常退出,释放锁
25: aload_2 // 加载异常对象
26: athrow // 重新抛出
// --- end ---
27: return
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
对应异常表:
Exception table:
from to target type
7 19 22 any // 同步块 (7-18) 发生任何异常, 跳转到 22
22 25 22 any // 异常处理块自身也可能抛异常 (虽然少见), 再次跳转到 22 (防止锁泄漏)
2
3
4
编译器通过异常表和冗余的
monitorexit
确保了锁在各种情况下都能被正确释放。