【JVM】字节码指令集
字节码指令集
文章目录
- 字节码指令集
- 概述
- 执行模型
- 字节码指令的基本结构
- 字节码与数据类型
- 指令按照用途区分
- 加载与存储指令
- 回顾局部变量表与操作数栈
- 局部变量表
- 操作数栈
- 局部变量压栈指令
- 常量入栈指令
- `const` 系列指令
- `push` 系列指令
- `ldc` 系列指令
- 出栈入局部变量表指令
- 算数指令
- 算术指令
- 位运算指令
- 比较指令
- 类型转换指令
- 宽化类型转化
- 窄化类型转化
- 对象的创建与访问指令
- 创建指令
- 字段访问指令
- 数组操作指令
- 类型检查指令
- 方法调用与返回指令
- 方法调用指令
- 方法返回指令
- 操作数栈管理指令
- 比较控制指令
- 条件跳转指令
- 比较条件跳转指令
- 多条件分支跳转指令
- 无条件跳转指令
- 异常处理指令
- 抛出异常指令
- 异常处理与异常表
- 同步控制指令
- 方法级同步
- 方法内指令指令序列的同步
概述
执行模型
do {
自动计算PC寄存器值+1;
根据PC寄存器的指示,从字节码流中取出操作码;
if (字节码存在操作数) 取出操作数,PC相应++
执行字节码对应的操作;
} while (PC未到最后)
字节码指令的基本结构
- 操作码(Opcode):
- 固定 1 字节(8位),取值范围
0x00
~0xFF
(共 256 种可能的操作码)。 - 例如:
iload_0
的操作码是0x1A
,iadd
的操作码是0x60
。
- 固定 1 字节(8位),取值范围
- 操作数(Operand):
- 不同指令需要的操作数长度不同(0~多个字节)。
- 操作数可能是局部变量索引、常量池索引、偏移量等。
字节码与数据类型
大部分类型相关的操作码中都有特殊的字符代表它是服务于哪个类型的数据。
字母 | 类型 |
---|---|
i | int |
l | long |
s | short |
b | byte |
c | char |
f | float |
d | double |
在很多指令中,byte、char、boolean以及short类型都是用int类型的指令进行操作的,因为他们都是整形家族的成员。
指令按照用途区分
- 加载与存储指令
- 算数指令
- 类型转换指令
- 对象的创建与访问指令
- 方法创建与访问指令
- 操作数栈管理指令
- 比较控制指令
- 异常处理指令
- 同步控制指令
加载与存储指令
回顾局部变量表与操作数栈
首先来回顾一下虚拟机栈的结构,加载与存储指令主要操作的对象就是函数栈帧中的局部变量表和操作数栈。
局部变量表
作用:存储方法执行过程中用到的局部变量(包括方法参数和方法内定义的变量)。
核心特性:
- 结构:
- 是一个按索引访问的数组,索引从
0
开始。 - 每个槽位(Slot)占用 32 位(4字节)。对于
long
和double
(64位类型),会占用两个连续的槽位。 - 槽位可以被复用(例如,局部变量的作用域结束后,槽位可能被其他变量重用)。
- 是一个按索引访问的数组,索引从
- 存储内容:
- 方法参数:非静态方法的第 0 号槽位是
this
引用,静态方法没有this
。 - 方法内定义的变量(包括基本类型、对象引用、返回地址等)。
- 方法参数:非静态方法的第 0 号槽位是
- 生命周期:
- 随方法调用创建(栈帧入栈),随方法结束销毁(栈帧出栈)。
示例代码分析:
public void example(int a, int b) {
int c = a + b;
long d = 100L;
}
对应的局部变量表:
索引 | 变量 | 类型 | 说明 |
---|---|---|---|
0 | this | 对象引用 | 非静态方法的隐含参数 |
1 | a | int | 方法参数 |
2 | b | int | 方法参数 |
3 | c | int | 方法内定义的变量 |
4 | d | long | 占用索引 4 和 5(两个槽位) |
操作数栈
作用:保存字节码指令执行过程中的临时操作数,是 JVM 基于栈的执行模型的核心。
核心特性:
- 结构:
- 后进先出(LIFO)的栈结构,最大深度在编译时确定。
- 每个栈单元(Entry)占用 32 位,
long
和double
占两个单元。
- 操作过程:
- 字节码指令从栈顶取出操作数,计算结果再压入栈顶。
- 例如,
iadd
指令会弹出两个int
值相加,再将结果压入栈。
- 生命周期:
- 随方法调用创建(栈帧入栈),随方法结束销毁(栈帧出栈)。
局部变量压栈指令
局部变量压栈指令将局部变量表中的数据压入操作数栈
- xload_
x
为操作数类型,取值为i、l、f、d、a,n
为要存入栈的变量在局部变量表中的下标索引,取值为0~3。
比如, aload_0
这个操作码的意思是将一个存放在局部变量表中下标索引为0的变量入操作数栈,变量类型为引用类型。其余前缀类型意义见字节码与数据类型。
- xload
x
为操作数类型,取值为i、l、f、d、a,n
为要存入栈的变量在局部变量表中的下标索引,当使用这个指令时,代表局部变量表中前四个槽位已经被占用,并且局部变量表中的前四个槽位中的变量都不是要入栈的变量。
xload 与 xload_ 的不同在于,xload_是没有操作数的,只有一操作码,只占用一个字节;而xload 中为操作数,比如:
iload 3 // 0x15 0x03(加载局部变量表索引 3 的 int 值,总占 2 字节)
举例分析如下:
public void load(int num, Object obj,long count,boolean flag,short[] arr) {
System.out.println(num);
System.out.println(obj);
System.out.println(count);
System.out.println(flag);
System.out.println(arr);
}
常量入栈指令
const
系列指令
用于将固定值直接压入操作数栈,无需额外操作数,占用 1 字节。
指令名称 | 操作码 (Hex) | 类型 | 值范围 | 示例说明 |
---|---|---|---|---|
iconst_m1 | 0x02 | int | -1 | iconst_m1 → 压入 -1 |
iconst_0 ~ iconst_5 | 0x03~0x08 | int | 0~5 | iconst_3 → 压入 3 |
lconst_0, lconst_1 | 0x09, 0x0A | long | 0L, 1L | lconst_1 → 压入 1L |
fconst_0 ~ fconst_2 | 0x0B~0x0D | float | 0.0f, 1.0f, 2.0f | fconst_2 → 压入 2.0f |
dconst_0, dconst_1 | 0x0E, 0x0F | double | 0.0, 1.0 | dconst_1 → 压入 1.0 |
aconst_null | 0x01 | 引用 | null | aconst_null → 压入 null |
push
系列指令
用于将指定范围内的整数值压入栈,需要显式操作数。
指令名称 | 操作码 (Hex) | 类型 | 值范围 | 占用字节数 | 示例说明 |
---|---|---|---|---|---|
bipush | 0x10 | int | -128~127 | 2 | bipush 100 → 压入 100 |
sipush | 0x11 | int | -32768~32767 | 3 | sipush 30000 → 压入30000 |
举例分析如下:
public void pushConstLdc() {
int i = -1;
int a = 5;
int b = 6;
int c = 127;
int d = 128;
int e = 32767;
int f = 32768;
}
ldc
系列指令
用于从常量池加载任意类型常量(如字符串、大数值、Class
对象等)。
指令名称 | 操作码 (Hex) | 类型 | 操作数范围 | 占用字节数 | 示例说明 |
---|---|---|---|---|---|
ldc | 0x12 | 通用(int/float/String等) | 常量池索引 0~255 | 2 | ldc #5 → 加载常量池第5项 |
ldc_w | 0x13 | 通用 | 常量池索引 0~65535 | 3 | ldc_w #1000 → 加载大索引常量 |
ldc2_w | 0x14 | long/double | 常量池索引 0~65535 | 3 | ldc2_w #7 → 加载 long 或 double |
ldc
支持 1 字节的常量池索引(最多 255)。ldc_w
和ldc2_w
支持 2 字节索引(扩展至 65535),后者专用于long
/double
。
举例分析如下:
public void constLdc() {
long a1 = 1;
long a2 = 2;
float b1 = 2;
float b2 = 3;
double c1 = 1;
double c2 = 2;
Date d = null;
}
出栈入局部变量表指令
将操作数栈顶值存入局部变量表
指令名称 | 操作码 (Hex) | 类型 | 索引范围 | 占用字节数 | 示例说明 |
---|---|---|---|---|---|
istore | 0x36 | int | 显式指定(1字节) | 2 | istore 3 → 存储栈顶int到索引3 |
istore_ | 0x3B~0x3E | int | 0~3 | 1 | istore_0 → 存储到索引0 |
lstore | 0x37 | long | 显式指定(1字节) | 2 | lstore 2 → 存储栈顶long到索引2 |
lstore_ | 0x3F~0x42 | long | 0~3 | 1 | lstore_1 → 存储到索引1 |
fstore | 0x38 | float | 显式指定(1字节) | 2 | fstore 4 → 存储栈顶float到索引4 |
fstore_ | 0x43~0x46 | float | 0~3 | 1 | fstore_2 → 存储到索引2 |
dstore | 0x39 | double | 显式指定(1字节) | 2 | dstore 1 → 存储栈顶double到索引1 |
dstore_ | 0x47~0x4A | double | 0~3 | 1 | dstore_3 → 存储到索引3 |
astore | 0x3A | 引用 | 显式指定(1字节) | 2 | astore 0 → 存储栈顶引用到索引0 |
astore_ | 0x4B~0x4E | 引用 | 0~3 | 1 | astore_0 → 存储到索引0 |
关键说明
- 带
_<n>
后缀的指令(如iload_0
、istore_1
):- 直接操作局部变量表的固定索引(
0
~3
),无需显式操作数,占用 1 字节。 - 用于优化高频操作的局部变量访问。
- 直接操作局部变量表的固定索引(
- 显式操作数的指令(如
iload
、astore
):- 需要 1 字节的操作数指定索引(范围
0
~255
),占用 2 字节。 - 超出
0
~3
的索引需使用此类指令。
- 需要 1 字节的操作数指定索引(范围
wide
指令:- 若局部变量索引超过
255
,需配合wide
指令扩展操作数为 2 字节:
- 若局部变量索引超过
wide iload 256 // 0xC4 0x15 0x01 0x00(总占4字节)
- 数据类型差异:
long
和double
类型占局部变量表的两个连续槽位(如索引n
和n+1
)。
举例分析如下:
public void store(int k, double d) {
int m = k + 2;
long l = 12;
String str = "atguigu";
float f = 10.0F;
d = 10;
}
首先该方法被调用的时候,形式参数k和d都是有确定的值,由于该方法不是静态方法,所以局部变量表中的第一个位置(槽位)存储this,而第二个位置存储k具体的值,由于只是分析,没有调用这个方法,所以全部使用的变量名称来代替具体的值,所以明白就好,继续来分析,然后第三个和第四个位置储存d具体的值,由于d是double类型,所以需要占据两个槽位,数据已经准备好了,那就来看字节码,首先iload_1是将局部变量表中下标为1的k值取出来压入操作数栈中,然后iconst_2是将常量池中的整型值2压入操作数栈,iadd让操作数栈弹出的k值和整型值2执行相加操作,之后将相加的结果值m压入操作数栈中,请注意的画法,在执行弹栈和压栈操作之后,并没有删除操作数栈中的k值和2,这是因为让我们知道具体的操作过程,所以故意为之,不过真正的操作是弹栈之后k值和2就会从操作数栈中弹出,之后操作数栈中就没有k值和2了,只有m值了,然后istore_4是将操作数栈中的m值弹出栈,然后放在局部变量表中下标为4的位置,idc2_w #13<12>代表将long型值12压入操作数栈,istore5是将值12弹栈之后放入局部变量表中下标为5的位置,由于12是long型,所以占据两个位置(槽位),ldc #15代表将字符串atguigu压入操作数栈,astore 7代表将字符串atguigu弹栈之后放入局部变量表中下标为7的位置,idc #16<10.0>代表将float类型数据10.0压入操作数栈,fstore 8代表将10.0弹出栈,然后放入局部变量表中下标为8的位置,idc2_w #17<10.0>代表将10.0压入操作数栈,dstore2代表将10.0弹出栈,之后将10.0放入下标为2和3的操作,毕竟这是double类型数据
还有一种槽位复用的情况:
public void foo(long l, float f) {
{
int i = 0;
}
{
String s = "Hello, World";
}
}
局部变量表中的槽位是可以复用的,也即一个局部变量出了它本身的作用域后,局部变量表将这个变量的槽位会分配给下一个变量。
算数指令
算术指令
类别 | 指令 | 操作码 (Hex) | 类型 | 操作数栈变化(执行前 → 执行后) | 功能描述 |
---|---|---|---|---|---|
加法 | iadd | 0x60 | int | …, val1, val2 → …, result | 弹出两个int,压入它们的和 |
ladd | 0x61 | long | …, val1, val2 → …, result | 弹出两个long,压入它们的和 | |
fadd | 0x62 | float | …, val1, val2 → …, result | 弹出两个float,压入它们的和 | |
dadd | 0x63 | double | …, val1, val2 → …, result | 弹出两个double,压入它们的和 | |
减法 | isub | 0x64 | int | …, val1, val2 → …, result | 弹出两个int,压入val1 - val2 |
lsub | 0x65 | long | …, val1, val2 → …, result | 弹出两个long,压入val1 - val2 | |
fsub | 0x66 | float | …, val1, val2 → …, result | 弹出两个float,压入val1 - val2 | |
dsub | 0x67 | double | …, val1, val2 → …, result | 弹出两个double,压入val1 - val2 | |
乘法 | imul | 0x68 | int | …, val1, val2 → …, result | 弹出两个int,压入它们的积 |
lmul | 0x69 | long | …, val1, val2 → …, result | 弹出两个long,压入它们的积 | |
fmul | 0x6A | float | …, val1, val2 → …, result | 弹出两个float,压入它们的积 | |
dmul | 0x6B | double | …, val1, val2 → …, result | 弹出两个double,压入它们的积 | |
除法 | idiv | 0x6C | int | …, val1, val2 → …, result | 弹出两个int,压入val1 / val2 |
ldiv | 0x6D | long | …, val1, val2 → …, result | 弹出两个long,压入val1 / val2 | |
fdiv | 0x6E | float | …, val1, val2 → …, result | 弹出两个float,压入val1 / val2 | |
ddiv | 0x6F | double | …, val1, val2 → …, result | 弹出两个double,压入val1 / val2 | |
求余 | irem | 0x70 | int | …, val1, val2 → …, result | 弹出两个int,压入val1 % val2 |
lrem | 0x71 | long | …, val1, val2 → …, result | 弹出两个long,压入val1 % val2 | |
frem | 0x72 | float | …, val1, val2 → …, result | 弹出两个float,压入val1 % val2 | |
drem | 0x73 | double | …, val1, val2 → …, result | 弹出两个double,压入val1 % val2 | |
取反 | ineg | 0x74 | int | …, val → …, result | 弹出int,压入其负值 |
lneg | 0x75 | long | …, val → …, result | 弹出long,压入其负值 | |
fneg | 0x76 | float | …, val → …, result | 弹出float,压入其负值 | |
dneg | 0x77 | double | …, val → …, result | 弹出double,压入其负值 | |
自增 | iinc | 0x84 | int | 无栈操作(直接修改局部变量) | 对局部变量表中的int值自增 |
位运算指令
类别 | 指令 | 操作码 (Hex) | 类型 | 操作数栈变化(执行前 → 执行后) | 功能描述 |
---|---|---|---|---|---|
位移 | ishl | 0x78 | int | …, val1, val2 → …, result | 左移val1(按val2的二进制位数) |
ishr | 0x7A | int | …, val1, val2 → …, result | 算术右移(符号位填充) | |
iushr | 0x7C | int | …, val1, val2 → …, result | 逻辑右移(零填充) | |
lshl | 0x79 | long | …, val1, val2 → …, result | long左移 | |
lshr | 0x7B | long | …, val1, val2 → …, result | long算术右移 | |
lushr | 0x7D | long | …, val1, val2 → …, result | long逻辑右移 | |
按位或 | ior | 0x80 | int | …, val1, val2 → …, result | 弹出两个int,压入按位或结果 |
lor | 0x81 | long | …, val1, val2 → …, result | 弹出两个long,压入按位或结果 | |
按位与 | iand | 0x7E | int | …, val1, val2 → …, result | 弹出两个int,压入按位与结果 |
land | 0x7F | long | …, val1, val2 → …, result | 弹出两个long,压入按位与结果 | |
按位异或 | ixor | 0x82 | int | …, val1, val2 → …, result | 弹出两个int,压入按位异或结果 |
lxor | 0x83 | long | …, val1, val2 → …, result | 弹出两个long,压入按位异或结果 |
比较指令
指令 | 操作码 (Hex) | 类型 | 操作数栈变化(执行前 → 执行后) | 功能描述 |
---|---|---|---|---|
lcmp | 0x94 | long | …, val1, val2 → …, result | 比较两个long值,压入结果(1 if val1 > val2, -1 if <, 0 if =) |
fcmpg | 0x96 | float | …, val1, val2 → …, result | 比较两个float值,若存在NaN则压入1(用于>或无序比较) |
fcmpl | 0x95 | float | …, val1, val2 → …, result | 比较两个float值,若存在NaN则压入-1(用于<或有序比较) |
dcmpg | 0x98 | double | …, val1, val2 → …, result | 比较两个double值,若存在NaN则压入1 |
dcmpl | 0x97 | double | …, val1, val2 → …, result | 比较两个double值,若存在NaN则压入-1 |
关键说明
- 自增指令
iinc
:- 直接操作局部变量表中的
int
值,无需操作数栈参与。 - 示例:
iinc 1 5
→ 将局部变量索引1的值加5。
- 直接操作局部变量表中的
- 比较指令差异:
fcmpg
和fcmpl
(及dcmpg
/dcmpl
)仅在遇到NaN
时返回值不同:fcmpg
返回1
(表示无序比较结果)。fcmpl
返回-1
(表示无效比较)。
- 位运算指令:
- 位移指令的位移位数由操作数栈顶的
int
值指定(例如,ishl
弹出两个值:被移位的int
和移位的位数)。
- 位移指令的位移位数由操作数栈顶的
类型转换指令
- 类型转换指令可以将两种不同的数值类型进行相互转换。
- 这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。
宽化类型转化
转换类型 | 字节码指令 | 精度损失可能性 | 异常情况 | 规则说明 |
---|---|---|---|---|
int → long | i2l | 否 | 无 | 精确转换(long 范围更大,完全容纳 int 值) |
int → float | i2f | 是 | 无 | 可能丢失最低有效位(float 尾数位有限,超出 2^24 时无法精确表示) |
int → double | i2d | 否 | 无 | 精确转换(double 尾数位足够容纳 int 的 32 位) |
long → float | l2f | 是 | 无 | 可能丢失精度(float 尾数位不足,超出 2^24 时近似舍入) |
long → double | l2d | 是 | 无 | 可能丢失精度(double 尾数位为 52,若 long 值超出 2^53 则无法精确表示) |
float → double | f2d | 否 | 无 | 精确转换(double 范围更大,精度更高) |
核心规则说明
- 转换方向:
- 允许从小范围类型向大范围类型自动转换(如
int
→long
→float
→double
)。 - 无需显式强制类型转换,由 JVM 隐式处理。
- 允许从小范围类型向大范围类型自动转换(如
- 精度损失:
- 无损失:
int → long
、int → double
、float → double
。 - 可能损失
int → float
:float
的 23 位尾数可能导致精度丢失(如16777217
转换后变为16777216
)。long → float/double
:long
的 64 位整数超出float
(24 位有效位)或double
(53 位有效位)的表示范围时,按 IEEE 754 最近舍入模式近似。
- 无损失:
- 异常处理:
- 不抛出任何运行时异常(即使发生精度丢失)。
窄化类型转化
转换类型 | 字节码指令 | 精度损失可能性 | 异常情况 | 规则说明 |
---|---|---|---|---|
int → byte | i2b | 是 | 无 | 保留低 8 位(符号位扩展) |
int → short | i2s | 是 | 无 | 保留低 16 位(符号位扩展) |
int → char | i2c | 是 | 无 | 保留低 16 位(零扩展) |
long → int | l2i | 是 | 无 | 保留低 32 位(符号位扩展) |
float → int | f2i | 是 | 无 | 向零舍入(若结果为 NaN 或超出 int 范围,返回 0 或极值) |
float → long | f2l | 是 | 无 | 向零舍入(若结果为 NaN 或超出 long 范围,返回 0 或极值) |
double → int | d2i | 是 | 无 | 向零舍入(若结果为 NaN 或超出 int 范围,返回 0 或极值) |
double → long | d2l | 是 | 无 | 向零舍入(若结果为 NaN 或超出 long 范围,返回 0 或极值) |
double → float | d2f | 是 | 无 | 向最接近数舍入(可能返回零、无穷大或 NaN) |
精度损失问题
- 所有窄化转换均可能导致精度丢失,包括:
- 符号改变(如正数转为负数)。
- 数量级丢失(如大整数截断为小范围类型)。
- 浮点舍入误差(如
3.14
→3
)。
- 不会抛出运行时异常(即使结果超出目标类型范围)。
补充说明
- 浮点数 → 整数(
f2i
/f2l
/d2i
/d2l
)规则
条件 | 转换结果 |
---|---|
浮点值为 NaN | 返回 0(int/long 类型)。 |
浮点值为有限值 | 向零舍入,若结果在目标类型范围内,返回该值;否则返回目标类型的最大/最小极值。 |
浮点值为正无穷大(+∞) | 返回目标类型的最大极值(如 int → 2147483647)。 |
浮点值为负无穷大(-∞) | 返回目标类型的最小极值(如 int → -2147483648)。 |
double
→float
(d2f
)规则**
条件 | 转换结果 |
---|---|
double 值绝对值太小 | 返回 ±0.0f(根据符号)。 |
double 值绝对值太大 | 返回 ±∞(根据符号)。 |
double 值为 NaN | 返回 Float.NaN。 |
其他情况 | 向最接近的 float 值舍入(IEEE 754 标准)。 |
- 浮点数 → short/byte 规则
先将浮点数通过 f2i/d2i
指令转化为int类型,然后再使用 i2f/i2d
进行转化。
示例
int a = (int) 3.7f; // f2i → 3(向零舍入)
byte b = (byte) 200; // i2b → -56(符号位扩展)
float c = (float) 1e40; // d2f → Float.POSITIVE_INFINITY
long d = (long) Double.NaN; // d2l → 0
对象的创建与访问指令
创建指令
创建类实例指令
指令名称 | 操作码 (Hex) | 操作数 | 类型 | 操作数栈变化(执行前 → 执行后) | 示例说明 | 注意事项 |
---|---|---|---|---|---|---|
new | 0xBB | 常量池索引(类的符号引用) | 类实例 | … → …, objectref | new #1 → 创建 Object 实例 | 仅分配内存,需后续调用 方法(如 invokespecial)初始化对象。 |
创建数组指令
- 基本类型数组
指令名称 | 操作码 (Hex) | 操作数 | 类型 | 操作数栈变化(执行前 → 执行后) | 示例说明 | 注意事项 |
---|---|---|---|---|---|---|
newarray | 0xBC | 基本类型代码(1字节) | 基本类型数组 | …, size → …, arrayref | newarray 10 → 创建 int[] | 类型代码见下表(如 T_INT=10)。 |
newarray
支持的基本类型代码:**
类型代码 (Hex) | 类型 |
---|---|
4 (0x04) | boolean |
5 (0x05) | char |
6 (0x06) | float |
7 (0x07) | double |
8 (0x08) | byte |
9 (0x09) | short |
10 (0x0A) | int |
11 (0x0B) | long |
引用类型数组
指令名称 | 操作码 (Hex) | 操作数 | 类型 | 操作数栈变化(执行前 → 执行后) | 示例说明 | 注意事项 |
---|---|---|---|---|---|---|
anewarray | 0xBD | 常量池索引(类的符号引用) | 引用类型数组 | …, size → …, arrayref | anewarray #3 → 创建 String[] | 数组元素初始化为 null。 |
多维数组
指令名称 | 操作码 (Hex) | 操作数 | 类型 | 操作数栈变化(执行前 → 执行后) | 示例说明 | 注意事项 |
---|---|---|---|---|---|---|
multianewarray | 0xC5 | 常量池索引(数组类符号引用)+ 维度数(1字节) | 多维数组 | …, size1, size2… → …, arrayref | multianewarray #5 3 → 创建三维数组 | 维度数必须与数组类型维度匹配,各维度大小依次从栈顶弹出。 |
关键说明
new
指令**:- 仅分配对象内存,对象字段初始化为默认值(如
0
、null
)。 - 需显式调用构造函数(通过
invokespecial <init>
)完成初始化。
- 仅分配对象内存,对象字段初始化为默认值(如
- 数组初始化:
- 基本类型数组元素初始化为
0
或false
。 - 引用类型数组元素初始化为
null
。 - 多维数组的每个维度需单独指定大小(如
multianewarray
需从栈顶依次弹出各维大小)。
- 基本类型数组元素初始化为
- 性能影响:
newarray
和anewarray
适用于一维数组,multianewarray
用于多维数组,但后者效率较低。
实例代码
// 创建类实例
Object obj = new Object(); // new #1 → invokespecial <init>
// 创建基本类型数组
int[] arr1 = new int[10]; // newarray 10
// 创建引用类型数组
String[] arr2 = new String[5]; // anewarray #3
// 创建三维数组
int[][][] arr3 = new int[2][3][4]; // multianewarray #5 3
字段访问指令
指令名称 | 操作码 (Hex) | 操作数 | 类型 | 操作数栈变化(执行前 → 执行后) | 功能说明 | 示例说明 |
---|---|---|---|---|---|---|
getstatic | 0xB2 | 常量池索引(Fieldref) | 静态字段 | … → …, value | 读取静态字段值并压入栈顶 | getstatic #8 → 加载静态变量值 |
putstatic | 0xB3 | 常量池索引(Fieldref) | 静态字段 | …, value → … | 将栈顶值写入静态字段 | putstatic #5 → 修改静态变量值 |
getfield | 0xB4 | 常量池索引(Fieldref) | 实例字段 | …, objectref → …, value | 读取对象实例字段值并压入栈顶 | getfield #10 → 读取对象的字段 |
putfield | 0xB5 | 常量池索引(Fieldref) | 实例字段 | …, objectref, value → … | 将栈顶值写入对象实例字段 | putfield #7 → 设置对象的字段值 |
核心说明
- 操作数来源:
- 所有字段访问指令均通过常量池中的
Fieldref
索引定位目标字段,包含字段所属类、字段名及描述符。
- 所有字段访问指令均通过常量池中的
- 操作数栈行为:
- 静态字段:
getstatic
无需对象引用,直接加载静态字段值到栈顶。putstatic
将栈顶值弹出并赋值给静态字段。
- 实例字段:
getfield
需要从操作数栈顶弹出对象引用(objectref
),再读取其字段值压入栈顶。putfield
需要依次弹出对象引用和字段值,将值赋给对象的字段。
- 静态字段:
数组操作指令
指令名称 | 操作类型 | 操作数栈变化(执行前 → 执行后) | 功能描述 |
---|---|---|---|
baload | 加载 | …, arrayref, index → …, value | 加载 byte 数组 的指定索引元素到操作数栈。 |
caload | 加载 | …, arrayref, index → …, value | 加载 char 数组 的指定索引元素到操作数栈。 |
saload | 加载 | …, arrayref, index → …, value | 加载 short 数组 的指定索引元素到操作数栈。 |
iaload | 加载 | …, arrayref, index → …, value | 加载 int 数组 的指定索引元素到操作数栈。 |
laload | 加载 | …, arrayref, index → …, value | 加载 long 数组 的指定索引元素到操作数栈(压入两个栈单元)。 |
faload | 加载 | …, arrayref, index → …, value | 加载 float 数组 的指定索引元素到操作数栈。 |
daload | 加载 | …, arrayref, index → …, value | 加载 double 数组 的指定索引元素到操作数栈(压入两个栈单元)。 |
aaload | 加载 | …, arrayref, index → …, value | 加载 引用类型数组 的指定索引元素到操作数栈。 |
bastore | 存储 | …, value, index, arrayref → … | 将操作数栈顶的 byte 值 存储到数组的指定索引位置。 |
castore | 存储 | …, value, index, arrayref → … | 将操作数栈顶的 char 值 存储到数组的指定索引位置。 |
sastore | 存储 | …, value, index, arrayref → … | 将操作数栈顶的 short 值 存储到数组的指定索引位置。 |
iastore | 存储 | …, value, index, arrayref → … | 将操作数栈顶的 int 值 存储到数组的指定索引位置。 |
lastore | 存储 | …, value (low), value (high), index, arrayref → … | 将操作数栈顶的 long 值 存储到数组的指定索引位置(弹出两个栈单元)。 |
fastore | 存储 | …, value, index, arrayref → … | 将操作数栈顶的 float 值 存储到数组的指定索引位置。 |
dastore | 存储 | …, value (low), value (high), index, arrayref → … | 将操作数栈顶的 double 值 存储到数组的指定索引位置(弹出两个栈单元)。 |
aastore | 存储 | …, value, index, arrayref → … | 将操作数栈顶的 引用值 存储到数组的指定索引位置。 |
arraylength | 长度 | …, arrayref → …, length | 弹出数组引用,压入数组长度(int 类型)。 |
关键说明
xaload
系列**:- 操作数栈需先压入 数组引用 和 索引,指令执行后弹出这两个值,并将对应元素压入栈顶。
long
/double
类型会占用两个栈单元(高位在前,低位在后)。
xastore
系列**:- 操作数栈需按顺序压入 值、索引、数组引用,指令执行后依次弹出这三个值,并将值写入数组。
long
/double
类型的值需占用两个栈单元(高位先入栈)。
arraylength
:- 若数组引用为
null
,会抛出NullPointerException
。
- 若数组引用为
类型检查指令
指令名称 | 操作码 (Hex) | 操作数栈变化(执行前 → 执行后) | 功能描述 | 示例说明 |
---|---|---|---|---|
instanceof | 0xC1 | …, objectref → …, result | 判断对象是否是某类/接口的实例,结果(1 是,0 否)压入栈顶。若对象为 null,结果为 0。 | instanceof java/lang/String → 判断对象是否为 String 实例。 |
checkcast | 0xC0 | …, objectref → …, objectref | 检查对象引用是否可强制转换为目标类型。若失败则抛出 ClassCastException;若成功,栈顶保留原引用。 | checkcast java/lang/String → 将对象强制转换为 String,失败时抛异常。 |
关键说明
instanceof
:- 操作数栈需压入对象引用,指令执行后弹出引用,压入
int
结果(1
或0
)。 - 对
null
对象返回0
,不会抛出异常。
- 操作数栈需压入对象引用,指令执行后弹出引用,压入
checkcast
:- 不改变操作数栈结构,仅校验类型是否兼容。
- 常用于显式类型转换(如
(String) obj
),若对象为null
,指令直接通过(不抛异常)。
方法调用与返回指令
方法调用指令
指令名称 | 分派方式 | 操作数栈变化(执行前 → 执行后) | 功能描述 | 典型应用场景 |
---|---|---|---|---|
invokevirtual | 动态绑定(虚方法) | …, objectref, [args] → … | 调用对象的实例方法,根据对象实际类型进行多态分派。 | 调用普通实例方法(如 obj.method(),可能触发子类重写)。 |
invokeinterface | 动态绑定(接口) | …, objectref, [args] → … | 调用接口方法,运行时搜索对象实现的接口方法。 | 通过接口引用调用方法(如 List list = new ArrayList(); list.add())。 |
invokespecial | 静态绑定 | …, objectref, [args] → … | 调用需特殊处理的方法,包括构造器、私有方法、父类方法。 | 构造器()、私有方法、super.method()(显式调用父类方法)。 |
invokestatic | 静态绑定 | …, [args] → … | 调用静态方法,直接绑定到类而非实例。 | 调用静态方法(如 Math.max() 或 obj.staticMethod(),无论是否用对象调用)。 |
invokedynamic | 动态绑定(用户定义) | …, [args] → … | 运行时动态解析方法,分派逻辑由用户引导方法决定(JDK 7+引入)。 | Lambda表达式、字符串拼接等动态语言特性(通常由编译器生成,开发者不直接使用)。 |
关键说明
invokevirtual
:- 多态核心指令:支持子类重写方法的动态分派。即使未被子类重写,也使用此指令(如
A a = new A(); a.method()
)。 - 操作数栈:需压入对象引用(
objectref
)和方法参数,执行后弹出这些值,方法返回值(若有)压入栈顶。
- 多态核心指令:支持子类重写方法的动态分派。即使未被子类重写,也使用此指令(如
invokeinterface
:- 接口方法调用:通过接口引用调用实际对象的实现方法。运行时需额外搜索方法表,性能略低于
invokevirtual
。 - 示例:
Runnable r = new MyTask(); r.run()
→invokeinterface
调用MyTask
的run()
。
- 接口方法调用:通过接口引用调用实际对象的实现方法。运行时需额外搜索方法表,性能略低于
invokespecial
:- 静态绑定:直接调用目标方法,无多态分派。
- 场景:
- 构造器(
new Object()
→invokespecial <init>
)。 - 私有方法(仅本类可访问)。
super.method()
(调用父类方法,可能跨多级父类查找)。
- 构造器(
invokestatic
:- 无对象依赖:操作数栈无需压入对象引用(
objectref
)。 - 调用方式无关:无论通过类名(
Class.staticMethod()
)或对象(obj.staticMethod()
)调用,均使用此指令。
- 无对象依赖:操作数栈无需压入对象引用(
invokedynamic
(补充说明):- 动态语言支持:允许运行时动态决定调用逻辑,提升灵活性。
- 开发者透明:通常由编译器生成(如 Lambda 表达式编译为匿名类时自动使用此指令)。
示例对比
代码示例 | 使用指令 | 说明 |
---|---|---|
obj.toString() | invokevirtual | 调用可能被子类重写的 toString() 方法。 |
list.add(“data”)(List接口) | invokeinterface | 通过接口引用调用实际实现类(如 ArrayList)的 add() 方法。 |
new Object() | invokespecial | 调用 Object 的构造器 。 |
super.print() | invokespecial | 显式调用父类的 print() 方法(即使子类重写该方法)。 |
Math.max(1, 2) | invokestatic | 调用静态方法,不依赖实例对象。 |
方法返回指令
返回类型 | 指令名称 | 操作数栈变化(执行前 → 执行后) | 关键行为 |
---|---|---|---|
void | return | … → … | 无返回值。直接退出当前方法栈帧,恢复调用者栈帧,转移控制权。 |
int(含 boolean、byte、char、short) | ireturn | …, value → … | 弹出栈顶 int 值,压入调用者操作数栈,丢弃当前栈其他元素。 |
long | lreturn | …, value → … | 弹出栈顶 long 值(占用两个栈单元),压入调用者操作数栈。 |
float | freturn | …, value → … | 弹出栈顶 float 值,压入调用者操作数栈。 |
double | dreturn | …, value → … | 弹出栈顶 double 值(占用两个栈单元),压入调用者操作数栈。 |
引用类型(如对象、数组) | areturn | …, objectref → … | 弹出栈顶引用,压入调用者操作数栈。 |
关键说明
- 通用行为:
- 所有返回指令(除
return
)会弹出当前栈顶的返回值,压入调用者的操作数栈。 - 方法返回后,当前栈帧被销毁,调用者的栈帧恢复,程序计数器(PC)指向调用指令的下一条指令。
- 所有返回指令(除
synchronized
方法**:- 若方法是
synchronized
,返回前会隐式执行monitorexit
指令,释放锁(确保线程安全)。
- 若方法是
- 特殊场景:
- 构造器返回:即使构造器无
return
语句,编译器会隐式添加return
指令。 - 异常返回:若方法因异常结束,返回值不会压入调用者栈,而是通过异常处理机制传递。
- 构造器返回:即使构造器无
示例
// int 返回类型
public int add(int a, int b) {
return a + b; // 编译后:ireturn
}
// void 返回类型
public void print() {
System.out.println("Hello"); // 编译后:return
}
// 引用返回类型
public String getName() {
return "Alice"; // 编译后:areturn
}
操作数栈管理指令
指令 | 功能 | 操作数栈变化(执行前 → 执行后) | 示例 |
---|---|---|---|
pop | 弹出栈顶 1 个 Slot 的数据(丢弃) | …, value → … | 弹出 int 或引用类型(如 pop 后栈顶减少 1 个元素)。 |
pop2 | 弹出栈顶 2 个 Slot 的数据(丢弃) | …, value1, value2 → … 或 …, value64 → …(针对 long/double) | 弹出 long 或两个 int(如 pop2 后栈顶减少 2 个元素)。 |
dup | 复制栈顶 1 个 Slot 的数据并压入栈顶 | …, value → …, value, value | 复制 int 值(如 dup 后栈顶重复 1 次该值)。 |
dup2 | 复制栈顶 2 个 Slot 的数据并压入栈顶 | …, value1, value2 → …, value1, value2, value1, value2 或 …, value64 → …, value64, value64 | 复制 long 或两个 int(如 dup2 后栈顶重复 2 次该值或一个 long)。 |
dup_x1 | 复制栈顶 1 个 Slot 的数据,并插入到栈顶第 2 个元素下方 | …, a, b → …, b, a, b | 栈为 [3, 5] → dup_x1 → [5, 3, 5](复制栈顶 5 插入到 3 下方)。 |
dup_x2 | 复制栈顶 1 个 Slot 的数据,并插入到栈顶第 3 个元素下方 | …, a, b, c → …, c, a, b, c | 栈为 [1, 2, 3] → dup_x2 → [3, 1, 2, 3](复制 3 插入到 1 下方)。 |
dup2_x1 | 复制栈顶 2 个 Slot 的数据,并插入到栈顶第 3 个元素下方 | …, a, b, c → …, b, c, a, b, c(假设 b 和 c 是 1 个 Slot 的数据) | 栈为 [1, 2, 3] → dup2_x1 → [2, 3, 1, 2, 3](复制 2,3 插入到 1 下方)。 |
dup2_x2 | 复制栈顶 2 个 Slot 的数据,并插入到栈顶第 4 个元素下方 | …, a, b, c, d → …, c, d, a, b, c, d | 栈为 [1, 2, 3, 4] → dup2_x2 → [3, 4, 1, 2, 3, 4](复制 3,4 插入到 1 下方)。 |
swap | 交换栈顶两个单 Slot 元素的位置(不支持 long/double) | …, a, b → …, b, a | 栈为 [5, 7] → swap → [7, 5](交换 5 和 7)。 |
nop | 空操作(字节码 0x00) | 无变化 | 用于占位或调试(如 nop 不改变栈状态)。 |
关键规则
- Slot 定义:
- 1 个 Slot = 32 位(如
int
、float
、引用
)。 long
和double
占用 2 个 Slot。
- 1 个 Slot = 32 位(如
- dup_x 系列插入位置公式:
- 插入位置 = dup 的系数 + x 的系数
dup_x1
:1 (dup) + 1 (x1) = 2 → 插入到栈顶第 2 个元素下方。dup2_x2
:2 (dup2) + 2 (x2) = 4 → 插入到栈顶第 4 个元素下方。
- 插入位置 = dup 的系数 + x 的系数
- swap 限制:
- 仅支持单 Slot 数据类型(如
int
、引用
),不支持交换long/double
。
- 仅支持单 Slot 数据类型(如
示例场景
- dup_x1: 栈初始:
[A, B]
→dup_x1
→[B, A, B]
- dup2_x1: 栈初始:
[A, B, C]
(假设B
和C
为 2 个 Slot) →dup2_x1
→[C, A, B, C]
- pop2: 栈初始:
[3.14(double)]
→pop2
→ 栈为空。
比较控制指令
条件跳转指令
指令名称 | 操作码 (Hex) | 条件判断规则(栈顶 int 值) | 操作数栈变化(执行前 → 执行后) | 示例场景 |
---|---|---|---|---|
ifeq | 0x99 | 等于 0 | …, value → … | if (a == 0) → iload a; ifeq offset |
ifne | 0x9A | 不等于 0 | …, value → … | if (a != 0) → iload a; ifne offset |
iflt | 0x9B | 小于 0 | …, value → … | if (a < 0) → iload a; iflt offset |
ifle | 0x9E | 小于等于 0 | …, value → … | if (a <= 0) → iload a; ifle offset |
ifgt | 0x9D | 大于 0 | …, value → … | if (a > 0) → iload a; ifgt offset |
ifge | 0x9C | 大于等于 0 | …, value → … | if (a >= 0) → iload a; ifge offset |
ifnull | 0xC6 | 对象引用为 null | …, ref → … | if (obj == null) → aload obj; ifnull offset |
ifnonnull | 0xC7 | 对象引用不为 null | …, ref → … | if (obj != null) → aload obj; ifnonnull offset |
关键说明
- 通用规则:
- 所有条件跳转指令:
- 弹出栈顶元素(
int
或对象引用),根据条件判断是否跳转到指定偏移量(16位有符号整数)。 - 跳转偏移量通过操作数计算:
目标地址 = 当前指令地址 + offset
。
- 弹出栈顶元素(
- 所有条件跳转指令:
- 数据类型处理:
boolean
:转换为int
后直接使用上表指令。
boolean flag = true;
if (flag) { ... } // 编译为:iconst_1; ifeq offset
long
/float
/double
:- 先执行对应类型的比较指令(
lcmp
/fcmpg
/dcmpl
等),生成int
结果(1
、0
、-1
)。 - 根据
int
结果使用条件跳转指令。
- 先执行对应类型的比较指令(
long a = 10L, b = 20L;
if (a > b) { ... } // 编译为:lload a; lload b; lcmp; ifle offset
- 对象引用判断:
ifnull
和ifnonnull
专门用于对象引用是否为null
的判断,不涉及数值比较。
比较指令与条件跳转的配合
数据类型 | 比较指令 | 生成结果(栈顶 int 值) | 条件跳转示例 |
---|---|---|---|
long | lcmp | 1(左 > 右), 0(相等), -1(左 < 右) | lcmp; ifgt offset(若左 > 右则跳转) |
float | fcmpg | 1(左 > 右 或存在 NaN), 0(相等), -1(左 < 右) | fcmpg; iflt offset(若左 < 右则跳转) |
double | dcmpl | 1(左 > 右), 0(相等), -1(左 < 右 或存在 NaN) | dcmpl; ifeq offset(若相等则跳转) |
示例代码分析
int a = 10;
if (a == 0) {
// 条件成立
}
对应字节码:
iload_1 // 加载变量a到栈顶
ifeq 6 // 栈顶值等于0则跳转到偏移量6
... // 条件不成立的代码
return
比较条件跳转指令
指令名称 | 操作码 (Hex) | 比较类型 | 操作数栈变化(执行前 → 执行后) | 比较规则(下部元素 vs 栈顶元素) | 示例场景 |
---|---|---|---|---|---|
if_icmpeq | 0x9F | int(含 byte/short/char) | …, a, b → … | a == b | if (x == y) → iload x; iload y; if_icmpeq offset |
if_icmpne | 0xA0 | int | …, a, b → … | a != b | if (x != y) → iload x; iload y; if_icmpne offset |
if_icmplt | 0xA1 | int | …, a, b → … | a < b | if (x < y) → iload x; iload y; if_icmplt offset |
if_icmple | 0xA4 | int | …, a, b → … | a <= b | if (x <= y) → iload x; iload y; if_icmple offset |
if_icmpgt | 0xA3 | int | …, a, b → … | a > b | if (x > y) → iload x; iload y; if_icmpgt offset |
if_icmpge | 0xA2 | int | …, a, b → … | a >= b | if (x >= y) → iload x; iload y; if_icmpge offset |
if_acmpeq | 0xA5 | 引用类型 | …, ref1, ref2 → … | ref1 == ref2(地址相同) | if (obj1 == obj2) → aload obj1; aload obj2; if_acmpeq offset |
if_acmpne | 0xA6 | 引用类型 | …, ref1, ref2 → … | ref1 != ref2(地址不同) | if (obj1 != obj2) → aload obj1; aload obj2; if_acmpne offset |
关键规则
- 操作数栈规则:
- 下部元素为左值:比较时总是用栈顶的 第二个元素(下部) 和 栈顶元素 进行比较。
- 比较后清空栈:无论是否跳转,比较的两个元素均被弹出,无数据入栈。
- 数据类型处理:
int
类型:包含byte
、short
、char
的隐式转换比较(如if (byteVar == intVar)
)。- 引用类型:仅比较对象地址(
==
或!=
),不涉及对象内容。 - 其他类型(
long
/float
/double
):需先通过比较指令(如lcmp
/fcmpg
)生成int
结果,再使用条件跳转指令(如ifeq
)。
- 跳转偏移量:
- 指令操作数为 16 位有符号整数,计算方式:
目标地址 = 当前指令地址 + offset
。
- 指令操作数为 16 位有符号整数,计算方式:
示例对比
代码示例 | 使用指令 | 操作数栈行为 |
---|---|---|
if (a == b)(int) | if_icmpeq | [a, b] → [],若 a == b 则跳转。 |
if (obj1 != obj2) | if_acmpne | [obj1, obj2] → [],若地址不同则跳转。 |
if (x >= y)(long 类型) | lcmp + ifle | [x_low, x_high, y_low, y_high] → [result],若 result >= 0 则跳转。 |
补充说明
long
/float
/double
比较流程**:- 执行
lcmp
/fcmpg
/dcmpl
等指令,生成int
结果(1
、0
、-1
)。 - 根据
int
结果使用ifeq
、ifgt
等条件跳转指令。
- 执行
long a = 100L, b = 200L;
if (a < b) { ... } // 编译为:lload a; lload b; lcmp; iflt offset
- 对象内容比较:
- 若需比较对象内容(如
String
的字符串值),需调用equals()
方法,无法直接使用if_acmpeq
。
- 若需比较对象内容(如
多条件分支跳转指令
指令名称 | tableswitch | lookupswitch |
---|---|---|
适用场景 | case 值连续且密集(如 1,2,3,4 ) | case 值离散或不连续(如 1,10,100 ) |
内部结构 | 存储起始值、结束值及对应的跳转偏移量数组 | 存储 case 值与跳转偏移量的键值对列表 |
查找方式 | 直接通过索引计算偏移量位置(O(1) 时间复杂度) | 线性搜索匹配 case 值(O(n) 时间复杂度) |
效率 | 高(适合大量连续值) | 低(适合少量离散值) |
操作数栈变化 | 弹出栈顶 int 类型的 index ,无数据入栈 | 弹出栈顶 int 类型的 index ,无数据入栈 |
跳转规则 | 若 index 在范围内,跳转到对应偏移量;否则跳转到 default | 若找到匹配 case ,跳转到对应偏移量;否则跳转到 default |
字节码示例 | tableswitch 1 to 4 ... | lookupswitch 3: 1→off1, 10→off2, 100→off3 ... |
关键说明
tableswitch
:- 适用条件:
case
值需为连续整数(如1,2,3,4
)。 - 底层实现:通过数学计算
index - min
快速定位偏移量,无需遍历。 - 内存占用:若
case
值范围大但实际值少(如1,1000
),可能浪费空间。
- 适用条件:
lookupswitch
:- 适用条件:
case
值为离散值(如1,10,100
)。 - 底层实现:存储
case-offset
键值对列表,需遍历查找匹配项。 - 优化:
case
值在字节码中按升序排列,但查找时仍为线性扫描。
- 适用条件:
- 默认跳转:两种指令均支持
default
分支,处理未匹配的情况。 - 编译器选择:
- 编译器会根据
case
值的分布自动选择指令。例如:
// 连续值 → tableswitch
switch (num) {
case 1: ... break;
case 2: ... break;
case 3: ... break;
}
// 离散值 → lookupswitch
switch (num) {
case 10: ... break;
case 100: ... break;
case 1000: ... break;
}
示例字节码
tableswitch
示例**
int num = 2;
switch (num) {
case 1: ... break;
case 2: ... break;
case 3: ... break;
default: ...
}
对应字节码:
tableswitch 1 to 3
1: L1
2: L2
3: L3
default: Ldefault
lookupswitch
示例**
int num = 100;
switch (num) {
case 10: ... break;
case 100: ... break;
case 1000: ... break;
default: ...
}
对应字节码:
lookupswitch 3
10: L1
100: L2
1000: L3
default: Ldefault
注意事项
case
值类型:仅支持int
(包括byte
/short
/char
隐式转换)。- 字符串
switch
:Java 7+ 的字符串switch
会被编译为基于哈希的lookupswitch
。 - 性能权衡:编译器优先选择
tableswitch
(效率高),仅在值不连续时使用lookupswitch
。
无条件跳转指令
指令名称 | 操作码 (Hex) | 操作数 | 操作数栈变化 | 功能描述 | 状态 |
---|---|---|---|---|---|
goto | 0xA7 | 2 字节(有符号偏移量) | 无变化 | 无条件跳转到指定偏移量(范围:-32768 ~ 32767)。 | 主流使用 |
goto_w | 0xC8 | 4 字节(有符号偏移量) | 无变化 | 无条件跳转到更大范围的偏移量(范围:-2^31 ~ 2^31-1)。 | 主流使用 |
jsr | 0xA8 | 2 字节(有符号偏移量) | … → …, returnAddress | 跳转到指定偏移量,并将下一条指令地址压入栈顶(用于 try-finally 的旧实现)。 | 已废弃 |
jsr_w | 0xC9 | 4 字节(有符号偏移量) | … → …, returnAddress | 类似 jsr,但支持更大偏移量(已废弃)。 | 已废弃 |
ret | 0xB1 | 1 字节(局部变量索引) | 无变化 | 从局部变量表中读取地址并跳转(需配合 jsr/jsr_w 使用)。 | 已废弃 |
关键说明
goto
与goto_w
:goto
:适用于大多数跳转场景(如循环、条件分支后的跳转),操作数范围较小。goto_w
:当跳转偏移量超过goto
的 2 字节范围时使用(如代码块非常长时)。
// 示例:循环中的 goto
while (true) {
// 编译后:goto 偏移量(循环体结束跳转回开头)
}
- **废弃指令
jsr
/jsr_w
/ret
:- 历史用途:用于实现
try-finally
,通过jsr
跳转到finally
代码块,执行后通过ret
返回。 - 问题:代码可读性差且易出错,现代 JVM 改用 复制
finally
代码到每个退出路径 或 异常表 实现。 - 替代方案:
- 历史用途:用于实现
// Java 7+ 使用 try-with-resources 或标准异常处理
try {
// 代码
} finally {
// 编译后:finally 代码被复制到每个可能的退出路径
}
- 操作数栈变化:
jsr
/jsr_w
:跳转前将下一条指令地址压入栈顶(供ret
使用)。ret
:从局部变量表中读取地址(由jsr
存储)并跳转。
注意事项
- 现代 JVM:避免使用
jsr
/ret
,这些指令在 Java 6 后逐渐被废弃,Java 7+ 的编译器完全不再生成它们。 goto
的灵活性**:支持向前或向后跳转,广泛用于break
、continue
、return
等流程控制。
异常处理指令
抛出异常指令
1. athrow
指令
指令名称 | 操作码 (Hex) | 操作数栈变化(执行前 → 执行后) | 功能描述 | 示例 |
---|---|---|---|---|
athrow | 0xBF | …, exception_ref → [empty] | 显式抛出异常对象(throw 语句的实现)。若异常未被捕获,当前方法终止,异常传播至调用者栈帧。 | throw new Exception(); → new #1; dup; invokespecial ; athrow |
- JVM 隐式抛出异常的指令示例
以下指令在检测到异常条件时会自动抛出运行时异常:
指令名称 | 异常类型 | 触发条件 | 示例场景 |
---|---|---|---|
idiv | ArithmeticException | 整数除法或取余运算中除数为 0 | int a = 10 / 0; → idiv 指令抛出异常 |
ldiv | ArithmeticException | 长整数除法或取余运算中除数为 0 | long b = 100L % 0L; → ldiv 指令抛出异常 |
aaload | NullPointerException | 访问 null 引用数组的索引 | String[] arr = null; String s = arr[0]; → aaload 指令抛出异常 |
iastore | ArrayIndexOutOfBoundsException | 数组索引越界 | int[] arr = new int[3]; arr[5] = 10; → iastore 指令抛出异常 |
checkcast | ClassCastException | 对象强制转换类型不兼容 | Object obj = “123”; Integer num = (Integer) obj; → checkcast 抛出异常 |
- 操作数栈的异常处理规则
场景 | 操作数栈行为 |
---|---|
显式抛出异常(athrow) | 清除当前方法的操作数栈,将异常对象压入调用者方法的操作数栈,终止当前方法执行。 |
隐式抛出异常 | 清除当前方法的操作数栈,将异常对象压入调用者方法的操作数栈,终止当前方法执行。 |
关键说明
- 异常传播机制:
- 若当前方法的异常表中未捕获异常,JVM 会依次清除当前栈帧,将异常对象传递至调用者栈帧,直到被
catch
块或默认异常处理器处理。
- 若当前方法的异常表中未捕获异常,JVM 会依次清除当前栈帧,将异常对象传递至调用者栈帧,直到被
- 操作数栈清空:
- 无论是显式还是隐式抛出异常,JVM 都会清空当前栈帧的操作数栈,确保调用者栈帧仅接收异常对象。
- 异常表:
- 每个方法编译后生成异常表,定义
try-catch
的范围和捕获类型。若异常匹配,跳转到catch
块执行,否则继续传播。
- 每个方法编译后生成异常表,定义
示例分析
显式抛出异常
public void demo() {
throw new RuntimeException("error");
}
对应字节码:
new #2 // 创建RuntimeException对象
dup // 复制引用(用于调用构造器)
ldc #3 // 加载字符串 "error"
invokespecial #4 // 调用RuntimeException.<init>
athrow // 抛出异常
隐式抛出异常
public void divide() {
int a = 10 / 0; // 触发idiv指令自动抛出ArithmeticException
}
对应字节码:
bipush 10
iconst_0
idiv // 除数为0,抛出异常
istore_1
return
异常处理与异常表
- 异常处理的核心机制:异常表
在 JVM 中,try-catch
或 try-finally
的异常处理通过 异常表(Exception Table) 实现,而非通过特定字节码指令。 异常表是方法字节码的一部分,定义了异常处理的逻辑范围和处理方式。
- 异常表的结构
每个方法的异常表由多个条目组成,每个条目包含以下字段:
字段 | 描述 |
---|---|
起始位置(start_pc) | try 块的起始指令位置(字节码偏移量)。 |
结束位置(end_pc) | try 块的结束指令位置(不包含该位置本身,即 [start_pc, end_pc))。 |
处理偏移量(handler_pc) | 异常处理代码的起始位置(指向 catch 或 finally 块的字节码偏移量)。 |
捕获类型(catch_type) | 常量池索引,指定捕获的异常类型(如 Exception)。若为 0,表示 finally 块。 |
- 异常处理流程
finally
块的实现
- 无论是否抛出异常,
finally
块代码都会执行。 - JVM 通过以下两种方式实现:
- 复制
finally
代码到所有退出路径: 在try
块的每个退出路径(如return
、throw
)前插入finally
代码。 - 异常表条目: 对于
try-finally
(无catch
),异常表条目中的catch_type
为0
,表示捕获所有异常类型,并在处理代码中执行finally
逻辑后重新抛出异常。
- 复制
- 显式声明异常(
throws
)- 若方法通过
throws
声明可能抛出的异常,字节码中会添加Exceptions
属性:
- 若方法通过
public void demo() throws IOException, SQLException { ... }
- 对应的字节码属性:
Exceptions:
throws java.io.IOException, java.sql.SQLException
- 作用:
- 供编译器和 JVM 验证方法调用是否处理了这些异常。
- 与方法内的异常表无关,仅用于声明方法的潜在异常类型。
示例分析
1. try-catch
的字节码
public void example() {
try {
System.out.println("try");
} catch (Exception e) {
System.out.println("catch");
}
}
异常表条目:
start_pc | end_pc | handler_pc | catch_type (常量池索引) |
---|---|---|---|
0 | 10 | 13 | Exception |
字节码逻辑:
try
块范围:字节码偏移量0~9
。- 若异常类型为
Exception
,跳转到handler_pc=13
(catch
块)。 - 执行
catch
块代码后继续执行后续指令。
2. try-finally
的字节码
public void example() {
try {
System.out.println("try");
} finally {
System.out.println("finally");
}
}
异常表条目:
start_pc | end_pc | handler_pc | catch_type |
---|---|---|---|
0 | 10 | 13 | 0 |
实现方式:
finally
块代码会被复制到try
块的每个退出路径(如return
前)。- 无论是否抛出异常,均执行
finally
代码。
关键区别
特性 | 异常表 | Exceptions 属性 |
---|---|---|
作用 | 处理 try-catch-finally 的代码逻辑 | 声明方法可能抛出的异常类型(throws) |
存储位置 | 方法的字节码中 | 方法的字节码属性表 |
运行时影响 | 控制异常捕获和执行流程 | 仅用于编译检查和文档约束 |
总结
- 异常表是 JVM 处理
try-catch-finally
的核心机制,通过定义代码范围和异常类型实现动态跳转。 finally
块通过代码复制或异常表确保始终执行。- 显式声明异常(
throws
)通过Exceptions
属性记录,与方法实际抛出的异常无直接关联。
同步控制指令
方法级同步
- 同步方法的实现机制
在 JVM 中,方法级同步(通过 synchronized
修饰的方法)通过 隐式锁机制 实现,而非显式使用 monitorenter
和 monitorexit
指令。其核心规则如下:
特性 | 说明 |
---|---|
锁的获取与释放 | JVM 在调用同步方法时自动获取锁,方法结束时(无论正常或异常)自动释放锁。 |
实现方式 | 通过方法的访问标志 ACC_SYNCHRONIZED 标识是否为同步方法。 |
锁对象 | 实例方法锁对象是 this,静态方法锁对象是类的 Class 对象。 |
- 同步方法的字节码特征
以下是一个同步方法的示例及其字节码分析:
public class SynchronizedTest {
private int i = 0;
public synchronized void add() {
i++;
}
}
对应字节码(通过 javap -v
反编译):
public synchronized void add();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED // 关键标识:ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // 访问字段 i
5: iconst_1
6: iadd
7: putfield #2 // 更新字段 i
10: return
关键点:
- 无显式同步指令:字节码中没有
monitorenter
和monitorexit
。 - 访问标志:
ACC_SYNCHRONIZED
明确标识该方法为同步方法。
- 同步方法与同步代码块的区别
特性 | 同步方法 | 同步代码块 |
---|---|---|
实现方式 | 通过 ACC_SYNCHRONIZED 隐式控制锁。 | 显式使用 monitorenter 和 monitorexit 指令。 |
锁范围 | 整个方法。 | 代码块内部(可精确控制锁的范围)。 |
字节码可见性 | 无显式锁指令,通过访问标志识别。 | 显式包含 monitorenter 和 monitorexit。 |
异常处理 | 方法结束时自动释放锁(包括异常抛出)。 | 需确保 monitorexit 在异常路径执行(通常通过 finally 块实现)。 |
适用场景 | 方法整体需要同步。 | 需要细粒度控制同步的代码段。 |
- 异常与锁释放
- 规则:若同步方法抛出异常且未内部处理,JVM 会在异常传播到方法外部前自动释放锁。
- 示例:
public synchronized void riskyMethod() {
if (error) {
throw new RuntimeException(); // 抛出异常,锁自动释放
}
}
- 如何验证方法是否为同步方法?
通过 javap -v
查看方法的访问标志:
// 同步方法示例
public synchronized void demo();
flags: ACC_PUBLIC, ACC_SYNCHRONIZED // 存在 ACC_SYNCHRONIZED
// 非同步方法示例
public void demo();
flags: ACC_PUBLIC // 无 ACC_SYNCHRONIZED
总结
- 同步方法通过
ACC_SYNCHRONIZED
隐式实现锁机制,无需显式指令,锁的获取和释放由 JVM 自动管理。 - 同步代码块需显式使用
monitorenter
和monitorexit
,适用于需要精确控制同步范围的场景。 - 工具验证:使用
javap -v
查看方法访问标志,区分同步与非同步方法。
方法内指令指令序列的同步
- 同步代码块的实现机制
通过 synchronized
修饰的代码块使用 显式锁机制,由 JVM 的 monitorenter
和 monitorexit
指令实现。以下是核心规则:
特性 | 说明 |
---|---|
锁的获取 | 线程通过 monitorenter 请求进入同步代码块,检查对象的监视器状态。 |
锁的释放 | 线程通过 monitorexit 退出同步代码块时释放锁。 |
锁对象 | 显式指定的对象(如 synchronized(obj)),实例方法默认是 this。 |
可重入性 | 同一线程可多次获取同一对象的锁(监视器计数器递增)。 |
- 对象监视器与计数器
- 监视器(Monitor):每个对象关联一个监视器,记录锁状态。
- 计数器:
- 计数器 = 0:对象未锁定,线程可获取锁。
- 计数器 > 0:对象已锁定。若当前线程是锁持有者,计数器递增(可重入);否则线程阻塞。
- 同步代码块的字节码流程
以下是一个示例及其字节码分析:
public class SynchronizedTest {
private int i = 0;
public void add(){
i++;
}
private Object obj = new Object();
public void subtract(){
synchronized (obj){
i--;
}
}
}
关键流程:
- 获取锁:
monitorenter
指令尝试获取锁对象的监视器。 - 执行代码:同步块内的操作(如
i--
)。 - 释放锁:
- 正常退出:
monitorexit
在代码块末尾释放锁(指令18)。 - 异常退出:若同步块内抛出异常,通过
Exception table
跳转到指令24释放锁,再抛出异常。
- 正常退出:
- 可重入性示例
同一线程多次进入同步代码块时,监视器计数器递增:
public void nestedSync() {
synchronized (lock) {
synchronized (lock) { // 同一线程重复获取锁
count++;
}
}
}
监视器状态变化:
- 第一次
monitorenter
→ 计数器从0
变为1
。 - 第二次
monitorenter
→ 计数器从1
变为2
。 - 第一次
monitorexit
→ 计数器从2
变为1
。 - 第二次
monitorexit
→ 计数器从1
变为0
(锁释放)。
- 同步代码块 vs 同步方法
特性 | 同步代码块 | 同步方法 |
---|---|---|
实现方式 | 显式使用 monitorenter 和 monitorexit | 隐式通过 ACC_SYNCHRONIZED 标志 |
锁对象控制 | 可指定任意对象 | 实例方法锁 this,静态方法锁 Class |
字节码可见性 | 显式锁指令 | 无显式指令,通过访问标志识别 |
灵活性 | 高(可控制同步范围) | 低(整个方法同步) |
-
关键注意事项
- 锁释放的严格性:即使同步块内抛出未捕获异常,
monitorexit
也会在异常处理路径中释放锁。 - 性能影响:频繁竞争锁可能导致线程阻塞,需合理设计同步范围。
- 死锁风险:多个线程以不同顺序获取多个锁时可能死锁,需避免嵌套锁的不一致获取顺序。
- 锁释放的严格性:即使同步块内抛出未捕获异常,
总结
monitorenter
和monitorexit
是 JVM 实现同步代码块的核心指令,显式控制锁的获取与释放。- 可重入性允许同一线程多次获取同一锁,避免自我阻塞。
- 异常处理确保锁在异常路径下仍被释放,避免死锁。
- 同步代码块相比同步方法更灵活,适用于需要细粒度控制的并发场景。
示例分析
操作数栈中的对象和monitorenter结合起来可以让线程获取锁,做法就是让对象的监视器标记从0变成1,这就代表该线程上锁了,然后在操作数栈的aload_1和monitorexit结合起来就可以让线程解锁,做法就是让对象的监视器标记从1变成0,这个解锁需要在方法退出之前完成,如果方法执行过程中出现了任何异常,将会跳到异常处理的字节码处执行相关代码,如果异常处理的字节码部分出现了问题,那就重新执行异常处理的字节码,这些内容都在异常表中写的很明确,其中异常表也在上面截图中。