【JVM】字节码指令集

字节码指令集

文章目录

概述

执行模型

c 复制代码
do {
    自动计算PC寄存器值+1;
    根据PC寄存器的指示,从字节码流中取出操作码;
    if (字节码存在操作数) 取出操作数,PC相应++
    执行字节码对应的操作;
} while (PC未到最后)

字节码指令的基本结构

  1. 操作码(Opcode):
    1. 固定 1 字节(8位),取值范围 0x00 ~ 0xFF(共 256 种可能的操作码)。
    2. 例如:iload_0 的操作码是 0x1Aiadd 的操作码是 0x60
  2. 操作数(Operand):
    1. 不同指令需要的操作数长度不同(0~多个字节)。
    2. 操作数可能是局部变量索引、常量池索引、偏移量等。

字节码与数据类型

大部分类型相关的操作码中都有特殊的字符代表它是服务于哪个类型的数据。

字母 类型
i int
l long
s short
b byte
c char
f float
d double

在很多指令中,byte、char、boolean以及short类型都是用int类型的指令进行操作的,因为他们都是整形家族的成员。

指令按照用途区分

  1. 加载与存储指令
  2. 算数指令
  3. 类型转换指令
  4. 对象的创建与访问指令
  5. 方法创建与访问指令
  6. 操作数栈管理指令
  7. 比较控制指令
  8. 异常处理指令
  9. 同步控制指令

加载与存储指令

回顾局部变量表与操作数栈

首先来回顾一下虚拟机栈的结构,加载与存储指令主要操作的对象就是函数栈帧中的局部变量表操作数栈

局部变量表

作用:存储方法执行过程中用到的局部变量(包括方法参数和方法内定义的变量)。

核心特性:

  1. 结构:
    1. 是一个按索引访问的数组,索引从 0 开始。
    2. 每个槽位(Slot)占用 32 位(4字节)。对于 longdouble(64位类型),会占用两个连续的槽位。
    3. 槽位可以被复用(例如,局部变量的作用域结束后,槽位可能被其他变量重用)。
  2. 存储内容:
    1. 方法参数:非静态方法的第 0 号槽位是 this 引用,静态方法没有 this
    2. 方法内定义的变量(包括基本类型、对象引用、返回地址等)。
  3. 生命周期:
    1. 随方法调用创建(栈帧入栈),随方法结束销毁(栈帧出栈)。

示例代码分析:

java 复制代码
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 基于栈的执行模型的核心。

核心特性:

  1. 结构:
    1. 后进先出(LIFO)的栈结构,最大深度在编译时确定。
    2. 每个栈单元(Entry)占用 32 位,longdouble 占两个单元。
  2. 操作过程:
    1. 字节码指令从栈顶取出操作数,计算结果再压入栈顶。
    2. 例如,iadd 指令会弹出两个 int 值相加,再将结果压入栈。
  3. 生命周期:
    1. 随方法调用创建(栈帧入栈),随方法结束销毁(栈帧出栈)。

局部变量压栈指令

局部变量压栈指令将局部变量表中的数据压入操作数栈

  1. xload_

x为操作数类型,取值为i、l、f、d、a,n为要存入栈的变量在局部变量表中的下标索引,取值为0~3。

比如, aload_0 这个操作码的意思是将一个存放在局部变量表中下标索引为0的变量入操作数栈,变量类型为引用类型。其余前缀类型意义见字节码与数据类型

  1. xload

x为操作数类型,取值为i、l、f、d、a,n为要存入栈的变量在局部变量表中的下标索引,当使用这个指令时,代表局部变量表中前四个槽位已经被占用,并且局部变量表中的前四个槽位中的变量都不是要入栈的变量。

xload 与 xload_ 的不同在于,xload_是没有操作数的,只有一操作码,只占用一个字节;而xload 中为操作数,比如:

java 复制代码
iload 3     // 0x15 0x03(加载局部变量表索引 3 的 int 值,总占 2 字节)

举例分析如下:

java 复制代码
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

举例分析如下:

java 复制代码
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_wldc2_w 支持 2 字节索引(扩展至 65535),后者专用于 long/double

举例分析如下:

java 复制代码
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

关键说明

  1. _<n> 后缀的指令(如 iload_0istore_1):
    1. 直接操作局部变量表的固定索引(0~3),无需显式操作数,占用 1 字节。
    2. 用于优化高频操作的局部变量访问。
  2. 显式操作数的指令(如 iloadastore):
    1. 需要 1 字节的操作数指定索引(范围 0~255),占用 2 字节。
    2. 超出 0~3 的索引需使用此类指令。
  3. wide 指令:
    1. 若局部变量索引超过 255,需配合 wide 指令扩展操作数为 2 字节:
plain 复制代码
wide iload 256  // 0xC4 0x15 0x01 0x00(总占4字节)
  1. 数据类型差异:
    1. longdouble 类型占局部变量表的两个连续槽位(如索引 nn+1)。

举例分析如下:

java 复制代码
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类型数据

还有一种槽位复用的情况:

java 复制代码
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

关键说明

  1. 自增指令 iinc
    1. 直接操作局部变量表中的int值,无需操作数栈参与。
    2. 示例:iinc 1 5 → 将局部变量索引1的值加5。
  2. 比较指令差异
    1. fcmpgfcmpl(及 dcmpg/dcmpl)仅在遇到NaN时返回值不同:
      • fcmpg 返回 1(表示无序比较结果)。
      • fcmpl 返回 -1(表示无效比较)。
  3. 位运算指令
    1. 位移指令的位移位数由操作数栈顶的int值指定(例如,ishl 弹出两个值:被移位的int和移位的位数)。

类型转换指令

  1. 类型转换指令可以将两种不同的数值类型进行相互转换。
  2. 这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。

宽化类型转化

转换类型 字节码指令 精度损失可能性 异常情况 规则说明
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 范围更大,精度更高)

核心规则说明

  1. 转换方向
    1. 允许从小范围类型向大范围类型自动转换(如 intlongfloatdouble)。
    2. 无需显式强制类型转换,由 JVM 隐式处理。
  2. 精度损失
    1. 无损失int → longint → doublefloat → double
    2. 可能损失
      • int → floatfloat 的 23 位尾数可能导致精度丢失(如 16777217 转换后变为 16777216)。
      • long → float/doublelong 的 64 位整数超出 float(24 位有效位)或 double(53 位有效位)的表示范围时,按 IEEE 754 最近舍入模式近似。
  3. 异常处理
    1. 不抛出任何运行时异常(即使发生精度丢失)。

窄化类型转化

转换类型 字节码指令 精度损失可能性 异常情况 规则说明
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.143)。
  • 不会抛出运行时异常(即使结果超出目标类型范围)。

补充说明

  1. 浮点数 → 整数(f2i/f2l/d2i/d2l)规则
条件 转换结果
浮点值为 NaN 返回 0(int/long 类型)。
浮点值为有限值 向零舍入,若结果在目标类型范围内,返回该值;否则返回目标类型的最大/最小极值。
浮点值为正无穷大(+∞) 返回目标类型的最大极值(如 int → 2147483647)。
浮点值为负无穷大(-∞) 返回目标类型的最小极值(如 int → -2147483648)。
  1. doublefloatd2f)规则**
条件 转换结果
double 值绝对值太小 返回 ±0.0f(根据符号)。
double 值绝对值太大 返回 ±∞(根据符号)。
double 值为 NaN 返回 Float.NaN。
其他情况 向最接近的 float 值舍入(IEEE 754 标准)。
  1. 浮点数 → short/byte 规则

先将浮点数通过 f2i/d2i指令转化为int类型,然后再使用 i2f/i2d 进行转化。

示例

java 复制代码
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)初始化对象。

创建数组指令

  1. 基本类型数组
指令名称 操作码 (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 → 创建三维数组 维度数必须与数组类型维度匹配,各维度大小依次从栈顶弹出。

关键说明

  1. new 指令**:
    1. 仅分配对象内存,对象字段初始化为默认值(如 0null)。
    2. 需显式调用构造函数(通过 invokespecial <init>)完成初始化。
  2. 数组初始化
    1. 基本类型数组元素初始化为 0false
    2. 引用类型数组元素初始化为 null
    3. 多维数组的每个维度需单独指定大小(如 multianewarray 需从栈顶依次弹出各维大小)。
  3. 性能影响
    1. newarrayanewarray 适用于一维数组,multianewarray 用于多维数组,但后者效率较低。

实例代码

java 复制代码
// 创建类实例
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 → 设置对象的字段值

核心说明

  1. 操作数来源
    1. 所有字段访问指令均通过常量池中的 Fieldref 索引定位目标字段,包含字段所属类、字段名及描述符。
  2. 操作数栈行为
    1. 静态字段:
      • getstatic 无需对象引用,直接加载静态字段值到栈顶。
      • putstatic 将栈顶值弹出并赋值给静态字段。
    2. 实例字段:
      • 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 类型)。

关键说明

  1. xaload 系列**:
    1. 操作数栈需先压入 数组引用索引,指令执行后弹出这两个值,并将对应元素压入栈顶。
    2. long/double 类型会占用两个栈单元(高位在前,低位在后)。
  2. xastore 系列**:
    1. 操作数栈需按顺序压入 索引数组引用,指令执行后依次弹出这三个值,并将值写入数组。
    2. long/double 类型的值需占用两个栈单元(高位先入栈)。
  3. arraylength
    1. 若数组引用为 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,失败时抛异常。

关键说明

  1. instanceof
    1. 操作数栈需压入对象引用,指令执行后弹出引用,压入 int 结果(10)。
    2. null 对象返回 0,不会抛出异常。
  2. checkcast
    1. 不改变操作数栈结构,仅校验类型是否兼容。
    2. 常用于显式类型转换(如 (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表达式、字符串拼接等动态语言特性(通常由编译器生成,开发者不直接使用)。

关键说明

  1. invokevirtual
    1. 多态核心指令 :支持子类重写方法的动态分派。即使未被子类重写,也使用此指令(如 A a = new A(); a.method())。
    2. 操作数栈 :需压入对象引用(objectref)和方法参数,执行后弹出这些值,方法返回值(若有)压入栈顶。
  2. invokeinterface
    1. 接口方法调用 :通过接口引用调用实际对象的实现方法。运行时需额外搜索方法表,性能略低于 invokevirtual
    2. 示例Runnable r = new MyTask(); r.run()invokeinterface 调用 MyTaskrun()
  3. invokespecial
    1. 静态绑定:直接调用目标方法,无多态分派。
    2. 场景
      • 构造器(new Object()invokespecial <init>)。
      • 私有方法(仅本类可访问)。
      • super.method()(调用父类方法,可能跨多级父类查找)。
  4. invokestatic
    1. 无对象依赖 :操作数栈无需压入对象引用(objectref)。
    2. 调用方式无关 :无论通过类名(Class.staticMethod())或对象(obj.staticMethod())调用,均使用此指令。
  5. invokedynamic(补充说明):
    1. 动态语言支持:允许运行时动态决定调用逻辑,提升灵活性。
    2. 开发者透明:通常由编译器生成(如 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 → ... 弹出栈顶引用,压入调用者操作数栈。

关键说明

  1. 通用行为
    1. 所有返回指令(除 return)会弹出当前栈顶的返回值,压入调用者的操作数栈。
    2. 方法返回后,当前栈帧被销毁,调用者的栈帧恢复,程序计数器(PC)指向调用指令的下一条指令。
  2. synchronized 方法**:
    1. 若方法是 synchronized,返回前会隐式执行 monitorexit 指令,释放锁(确保线程安全)。
  3. 特殊场景
    1. 构造器返回 :即使构造器无 return 语句,编译器会隐式添加 return 指令。
    2. 异常返回:若方法因异常结束,返回值不会压入调用者栈,而是通过异常处理机制传递。

示例

typescript 复制代码
// 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 不改变栈状态)。

关键规则

  1. Slot 定义
    1. 1 个 Slot = 32 位(如 intfloat引用)。
    2. longdouble 占用 2 个 Slot。
  2. dup_x 系列插入位置公式
    1. 插入位置 = dup 的系数 + x 的系数
      • dup_x1:1 (dup) + 1 (x1) = 2 → 插入到栈顶第 2 个元素下方。
      • dup2_x2:2 (dup2) + 2 (x2) = 4 → 插入到栈顶第 4 个元素下方。
  3. swap 限制
    1. 仅支持单 Slot 数据类型(如 int引用),不支持交换 long/double

示例场景

  • dup_x1 : 栈初始:[A, B]dup_x1[B, A, B]
  • dup2_x1 : 栈初始:[A, B, C](假设 BC 为 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

关键说明

  1. 通用规则
    1. 所有条件跳转指令
      • 弹出栈顶元素(int 或对象引用),根据条件判断是否跳转到指定偏移量(16位有符号整数)。
      • 跳转偏移量通过操作数计算:目标地址 = 当前指令地址 + offset
  2. 数据类型处理
    1. boolean:转换为 int 后直接使用上表指令。
typescript 复制代码
boolean flag = true;
if (flag) { ... }  // 编译为:iconst_1; ifeq offset
  1. long/float/double
    • 先执行对应类型的比较指令(lcmp/fcmpg/dcmpl等),生成 int 结果(10-1)。
    • 根据 int 结果使用条件跳转指令。
typescript 复制代码
long a = 10L, b = 20L;
if (a > b) { ... }  // 编译为:lload a; lload b; lcmp; ifle offset
  1. 对象引用判断
    1. ifnullifnonnull 专门用于对象引用是否为 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(若相等则跳转)

示例代码分析

typescript 复制代码
int a = 10;
if (a == 0) {
    // 条件成立
}

对应字节码

typescript 复制代码
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

关键规则

  1. 操作数栈规则
    1. 下部元素为左值 :比较时总是用栈顶的 第二个元素(下部)栈顶元素 进行比较。
    2. 比较后清空栈:无论是否跳转,比较的两个元素均被弹出,无数据入栈。
  2. 数据类型处理
    1. int 类型:包含 byteshortchar 的隐式转换比较(如 if (byteVar == intVar))。
    2. 引用类型 :仅比较对象地址(==!=),不涉及对象内容。
    3. 其他类型long/float/double):需先通过比较指令(如 lcmp/fcmpg)生成 int 结果,再使用条件跳转指令(如 ifeq)。
  3. 跳转偏移量
    1. 指令操作数为 16 位有符号整数 ,计算方式:目标地址 = 当前指令地址 + offset

示例对比

代码示例 使用指令 操作数栈行为
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 结果(10-1)。
    • 根据 int 结果使用 ifeqifgt 等条件跳转指令。
typescript 复制代码
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 ...

关键说明

  1. tableswitch
    1. 适用条件case 值需为连续整数(如 1,2,3,4)。
    2. 底层实现 :通过数学计算 index - min 快速定位偏移量,无需遍历。
    3. 内存占用 :若 case 值范围大但实际值少(如 1,1000),可能浪费空间。
  2. lookupswitch
    1. 适用条件case 值为离散值(如 1,10,100)。
    2. 底层实现 :存储 case-offset 键值对列表,需遍历查找匹配项。
    3. 优化case 值在字节码中按升序排列,但查找时仍为线性扫描。
  3. 默认跳转 :两种指令均支持 default 分支,处理未匹配的情况。
  4. 编译器选择
  • 编译器会根据 case 值的分布自动选择指令。例如:
java 复制代码
// 连续值 → tableswitch
switch (num) {
    case 1: ... break;
    case 2: ... break;
    case 3: ... break;
}
// 离散值 → lookupswitch
switch (num) {
    case 10: ... break;
    case 100: ... break;
    case 1000: ... break;
}

示例字节码

  1. tableswitch示例**
plain 复制代码
int num = 2;
switch (num) {
    case 1: ... break;
    case 2: ... break;
    case 3: ... break;
    default: ...
}

对应字节码:

plain 复制代码
tableswitch 1 to 3
    1: L1
    2: L2
    3: L3
    default: Ldefault
  1. lookupswitch示例**
plain 复制代码
int num = 100;
switch (num) {
    case 10: ... break;
    case 100: ... break;
    case 1000: ... break;
    default: ...
}

对应字节码:

plain 复制代码
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 使用)。 已废弃

关键说明

  1. gotogoto_w
    1. goto:适用于大多数跳转场景(如循环、条件分支后的跳转),操作数范围较小。
    2. goto_w:当跳转偏移量超过 goto 的 2 字节范围时使用(如代码块非常长时)。
java 复制代码
// 示例:循环中的 goto
while (true) {
    // 编译后:goto 偏移量(循环体结束跳转回开头)
}
  1. **废弃指令 jsr/jsr_w/ret
    1. 历史用途 :用于实现 try-finally,通过 jsr 跳转到 finally 代码块,执行后通过 ret 返回。
    2. 问题 :代码可读性差且易出错,现代 JVM 改用 复制 finally 代码到每个退出路径异常表 实现。
    3. 替代方案
java 复制代码
// Java 7+ 使用 try-with-resources 或标准异常处理
try {
    // 代码
} finally {
    // 编译后:finally 代码被复制到每个可能的退出路径
}
  1. 操作数栈变化
    1. jsr/jsr_w:跳转前将下一条指令地址压入栈顶(供 ret 使用)。
    2. ret:从局部变量表中读取地址(由 jsr 存储)并跳转。

注意事项

  • 现代 JVM :避免使用 jsr/ret,这些指令在 Java 6 后逐渐被废弃,Java 7+ 的编译器完全不再生成它们。
  • goto 的灵活性**:支持向前或向后跳转,广泛用于 breakcontinuereturn 等流程控制。

异常处理指令

抛出异常指令

1. athrow 指令

指令名称 操作码 (Hex) 操作数栈变化(执行前 → 执行后) 功能描述 示例
athrow 0xBF ..., exception_ref → [empty] 显式抛出异常对象(throw 语句的实现)。若异常未被捕获,当前方法终止,异常传播至调用者栈帧。 throw new Exception(); → new #1; dup; invokespecial ; athrow
  1. 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 抛出异常
  1. 操作数栈的异常处理规则
场景 操作数栈行为
显式抛出异常(athrow) 清除当前方法的操作数栈,将异常对象压入调用者方法的操作数栈,终止当前方法执行。
隐式抛出异常 清除当前方法的操作数栈,将异常对象压入调用者方法的操作数栈,终止当前方法执行。

关键说明

  1. 异常传播机制
    1. 若当前方法的异常表中未捕获异常,JVM 会依次清除当前栈帧,将异常对象传递至调用者栈帧,直到被 catch 块或默认异常处理器处理。
  2. 操作数栈清空
    1. 无论是显式还是隐式抛出异常,JVM 都会清空当前栈帧的操作数栈,确保调用者栈帧仅接收异常对象。
  3. 异常表
    1. 每个方法编译后生成异常表,定义 try-catch 的范围和捕获类型。若异常匹配,跳转到 catch 块执行,否则继续传播。

示例分析

显式抛出异常

typescript 复制代码
public void demo() {
    throw new RuntimeException("error");
}

对应字节码

typescript 复制代码
new #2          // 创建RuntimeException对象
dup             // 复制引用(用于调用构造器)
ldc #3          // 加载字符串 "error"
invokespecial #4 // 调用RuntimeException.<init>
athrow          // 抛出异常

隐式抛出异常

typescript 复制代码
public void divide() {
    int a = 10 / 0; // 触发idiv指令自动抛出ArithmeticException
}

对应字节码

typescript 复制代码
bipush 10
iconst_0
idiv           // 除数为0,抛出异常
istore_1
return

异常处理与异常表

  1. 异常处理的核心机制:异常表

在 JVM 中,try-catchtry-finally 的异常处理通过 异常表(Exception Table) 实现,而非通过特定字节码指令。 异常表是方法字节码的一部分,定义了异常处理的逻辑范围和处理方式。

  1. 异常表的结构

每个方法的异常表由多个条目组成,每个条目包含以下字段:

字段 描述
起始位置(start_pc) try 块的起始指令位置(字节码偏移量)。
结束位置(end_pc) try 块的结束指令位置(不包含该位置本身,即 [start_pc, end_pc))。
处理偏移量(handler_pc) 异常处理代码的起始位置(指向 catch 或 finally 块的字节码偏移量)。
捕获类型(catch_type) 常量池索引,指定捕获的异常类型(如 Exception)。若为 0,表示 finally 块。
  1. 异常处理流程
  1. finally 块的实现
  • 无论是否抛出异常finally 块代码都会执行。
  • JVM 通过以下两种方式实现:
    • 复制 finally 代码到所有退出路径 : 在 try 块的每个退出路径(如 returnthrow)前插入 finally 代码。
    • 异常表条目 : 对于 try-finally(无 catch),异常表条目中的 catch_type0,表示捕获所有异常类型,并在处理代码中执行 finally 逻辑后重新抛出异常。
  1. 显式声明异常(throws
    1. 若方法通过 throws 声明可能抛出的异常,字节码中会添加 Exceptions 属性:
typescript 复制代码
public void demo() throws IOException, SQLException { ... }
  1. 对应的字节码属性
typescript 复制代码
Exceptions:
  throws java.io.IOException, java.sql.SQLException
  1. 作用
    • 供编译器和 JVM 验证方法调用是否处理了这些异常。
    • 与方法内的异常表无关,仅用于声明方法的潜在异常类型。

示例分析

1. try-catch 的字节码

typescript 复制代码
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

字节码逻辑

  1. try 块范围:字节码偏移量 0~9
  2. 若异常类型为 Exception,跳转到 handler_pc=13catch 块)。
  3. 执行 catch 块代码后继续执行后续指令。

2. try-finally 的字节码

typescript 复制代码
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 属性记录,与方法实际抛出的异常无直接关联。

同步控制指令

方法级同步

  1. 同步方法的实现机制

在 JVM 中,方法级同步 (通过 synchronized 修饰的方法)通过 隐式锁机制 实现,而非显式使用 monitorentermonitorexit 指令。其核心规则如下:

特性 说明
锁的获取与释放 JVM 在调用同步方法时自动获取锁,方法结束时(无论正常或异常)自动释放锁。
实现方式 通过方法的访问标志 ACC_SYNCHRONIZED 标识是否为同步方法。
锁对象 实例方法锁对象是 this,静态方法锁对象是类的 Class 对象。
  1. 同步方法的字节码特征

以下是一个同步方法的示例及其字节码分析:

java 复制代码
public class SynchronizedTest {
    private int i = 0;
    public synchronized void add() {
        i++;
    }
}

对应字节码 (通过 javap -v 反编译):

yaml 复制代码
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

关键点

  • 无显式同步指令 :字节码中没有 monitorentermonitorexit
  • 访问标志ACC_SYNCHRONIZED 明确标识该方法为同步方法。
  1. 同步方法与同步代码块的区别
特性 同步方法 同步代码块
实现方式 通过 ACC_SYNCHRONIZED 隐式控制锁。 显式使用 monitorenter 和 monitorexit 指令。
锁范围 整个方法。 代码块内部(可精确控制锁的范围)。
字节码可见性 无显式锁指令,通过访问标志识别。 显式包含 monitorenter 和 monitorexit。
异常处理 方法结束时自动释放锁(包括异常抛出)。 需确保 monitorexit 在异常路径执行(通常通过 finally 块实现)。
适用场景 方法整体需要同步。 需要细粒度控制同步的代码段。
  1. 异常与锁释放
  • 规则:若同步方法抛出异常且未内部处理,JVM 会在异常传播到方法外部前自动释放锁。
  • 示例
java 复制代码
public synchronized void riskyMethod() {
    if (error) {
        throw new RuntimeException(); // 抛出异常,锁自动释放
    }
}
  1. 如何验证方法是否为同步方法?

通过 javap -v 查看方法的访问标志:

typescript 复制代码
// 同步方法示例
public synchronized void demo();
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED  // 存在 ACC_SYNCHRONIZED
  
// 非同步方法示例
public void demo();
  flags: ACC_PUBLIC                     // 无 ACC_SYNCHRONIZED

总结

  • 同步方法 通过 ACC_SYNCHRONIZED 隐式实现锁机制,无需显式指令,锁的获取和释放由 JVM 自动管理。
  • 同步代码块 需显式使用 monitorentermonitorexit,适用于需要精确控制同步范围的场景。
  • 工具验证 :使用 javap -v 查看方法访问标志,区分同步与非同步方法。

方法内指令指令序列的同步

  1. 同步代码块的实现机制

通过 synchronized 修饰的代码块使用 显式锁机制 ,由 JVM 的 monitorentermonitorexit 指令实现。以下是核心规则:

特性 说明
锁的获取 线程通过 monitorenter 请求进入同步代码块,检查对象的监视器状态。
锁的释放 线程通过 monitorexit 退出同步代码块时释放锁。
锁对象 显式指定的对象(如 synchronized(obj)),实例方法默认是 this。
可重入性 同一线程可多次获取同一对象的锁(监视器计数器递增)。
  1. 对象监视器与计数器
  • 监视器(Monitor):每个对象关联一个监视器,记录锁状态。
  • 计数器
    • 计数器 = 0:对象未锁定,线程可获取锁。
    • 计数器 > 0:对象已锁定。若当前线程是锁持有者,计数器递增(可重入);否则线程阻塞。
  1. 同步代码块的字节码流程

以下是一个示例及其字节码分析:

java 复制代码
public class SynchronizedTest {

    private int i = 0;
    public void add(){
        i++;
    }


    private Object obj = new Object();
    public void subtract(){

        synchronized (obj){
            i--;
        }
    }
}

关键流程

  1. 获取锁monitorenter 指令尝试获取锁对象的监视器。
  2. 执行代码 :同步块内的操作(如 i--)。
  3. 释放锁
    1. 正常退出monitorexit 在代码块末尾释放锁(指令18)。
    2. 异常退出 :若同步块内抛出异常,通过 Exception table 跳转到指令24释放锁,再抛出异常。

  1. 可重入性示例

同一线程多次进入同步代码块时,监视器计数器递增:

java 复制代码
public void nestedSync() {
    synchronized (lock) {
        synchronized (lock) { // 同一线程重复获取锁
            count++;
        }
    }
}

监视器状态变化

  • 第一次 monitorenter → 计数器从 0 变为 1
  • 第二次 monitorenter → 计数器从 1 变为 2
  • 第一次 monitorexit → 计数器从 2 变为 1
  • 第二次 monitorexit → 计数器从 1 变为 0(锁释放)。
  1. 同步代码块 vs 同步方法
特性 同步代码块 同步方法
实现方式 显式使用 monitorenter 和 monitorexit 隐式通过 ACC_SYNCHRONIZED 标志
锁对象控制 可指定任意对象 实例方法锁 this,静态方法锁 Class
字节码可见性 显式锁指令 无显式指令,通过访问标志识别
灵活性 高(可控制同步范围) 低(整个方法同步)
  1. 关键注意事项

    1. 锁释放的严格性 :即使同步块内抛出未捕获异常,monitorexit 也会在异常处理路径中释放锁。
    2. 性能影响:频繁竞争锁可能导致线程阻塞,需合理设计同步范围。
    3. 死锁风险:多个线程以不同顺序获取多个锁时可能死锁,需避免嵌套锁的不一致获取顺序。

总结

  • monitorentermonitorexit 是 JVM 实现同步代码块的核心指令,显式控制锁的获取与释放。
  • 可重入性允许同一线程多次获取同一锁,避免自我阻塞。
  • 异常处理确保锁在异常路径下仍被释放,避免死锁。
  • 同步代码块相比同步方法更灵活,适用于需要细粒度控制的并发场景。

示例分析

操作数栈中的对象和monitorenter结合起来可以让线程获取锁,做法就是让对象的监视器标记从0变成1,这就代表该线程上锁了,然后在操作数栈的aload_1和monitorexit结合起来就可以让线程解锁,做法就是让对象的监视器标记从1变成0,这个解锁需要在方法退出之前完成,如果方法执行过程中出现了任何异常,将会跳到异常处理的字节码处执行相关代码,如果异常处理的字节码部分出现了问题,那就重新执行异常处理的字节码,这些内容都在异常表中写的很明确,其中异常表也在上面截图中。

相关推荐
lamdaxu30 分钟前
分布式调用(02)
后端
daiyunchao31 分钟前
让Pomelo支持HTTP协议
后端
芒猿君1 小时前
AQS——同步器框架之源
后端
SaebaRyo1 小时前
手把手教你在网站中启用https和http2
后端·nginx·https
Forget the Dream1 小时前
设计模式之迭代器模式
java·c++·设计模式·迭代器模式
咩咩觉主1 小时前
C# &Unity 唐老狮 No.7 模拟面试题
开发语言·unity·c#
大丈夫在世当日食一鲲1 小时前
Java中用到的设计模式
java·开发语言·设计模式
A-Kamen1 小时前
Spring Boot拦截器(Interceptor)与过滤器(Filter)深度解析:区别、实现与实战指南
java·spring boot·后端
练川1 小时前
Stream特性(踩坑):惰性执行、不修改原始数据源
java·stream
豆豆酱1 小时前
Transformer结构详解
后端