当前位置: 首页 > article >正文

【JVM】字节码指令集

字节码指令集

文章目录

  • 字节码指令集
    • 概述
      • 执行模型
      • 字节码指令的基本结构
      • 字节码与数据类型
      • 指令按照用途区分
    • 加载与存储指令
      • 回顾局部变量表与操作数栈
        • 局部变量表
        • 操作数栈
      • 局部变量压栈指令
      • 常量入栈指令
        • `const` 系列指令
        • `push` 系列指令
        • `ldc` 系列指令
      • 出栈入局部变量表指令
    • 算数指令
      • 算术指令
      • 位运算指令
      • 比较指令
    • 类型转换指令
      • 宽化类型转化
      • 窄化类型转化
    • 对象的创建与访问指令
      • 创建指令
      • 字段访问指令
      • 数组操作指令
      • 类型检查指令
    • 方法调用与返回指令
      • 方法调用指令
      • 方法返回指令
    • 操作数栈管理指令
    • 比较控制指令
      • 条件跳转指令
      • 比较条件跳转指令
      • 多条件分支跳转指令
      • 无条件跳转指令
    • 异常处理指令
      • 抛出异常指令
      • 异常处理与异常表
    • 同步控制指令
      • 方法级同步
      • 方法内指令指令序列的同步

概述

执行模型

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. 操作数可能是局部变量索引、常量池索引、偏移量等。

字节码与数据类型

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

字母类型
iint
llong
sshort
bbyte
cchar
ffloat
ddouble

在很多指令中,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. 随方法调用创建(栈帧入栈),随方法结束销毁(栈帧出栈)。

示例代码分析:

public void example(int a, int b) {
    int c = a + b;
    long d = 100L;
}

对应的局部变量表:

索引变量类型说明
0this对象引用非静态方法的隐含参数
1aint方法参数
2bint方法参数
3cint方法内定义的变量
4dlong占用索引 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 中为操作数,比如:

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_m10x02int-1iconst_m1 → 压入 -1
iconst_0 ~ iconst_50x03~0x08int0~5iconst_3 → 压入 3
lconst_0, lconst_10x09, 0x0Along0L, 1Llconst_1 → 压入 1L
fconst_0 ~ fconst_20x0B~0x0Dfloat0.0f, 1.0f, 2.0ffconst_2 → 压入 2.0f
dconst_0, dconst_10x0E, 0x0Fdouble0.0, 1.0dconst_1 → 压入 1.0
aconst_null0x01引用nullaconst_null → 压入 null
push 系列指令

用于将指定范围内的整数值压入栈,需要显式操作数。

指令名称操作码 (Hex)类型值范围占用字节数示例说明
bipush0x10int-128~1272bipush 100 → 压入 100
sipush0x11int-32768~327673sipush 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)类型操作数范围占用字节数示例说明
ldc0x12通用(int/float/String等)常量池索引 0~2552ldc #5 → 加载常量池第5项
ldc_w0x13通用常量池索引 0~655353ldc_w #1000 → 加载大索引常量
ldc2_w0x14long/double常量池索引 0~655353ldc2_w #7 → 加载 long 或 double
  • ldc 支持 1 字节的常量池索引(最多 255)。
  • ldc_wldc2_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)类型索引范围占用字节数示例说明
istore0x36int显式指定(1字节)2istore 3 → 存储栈顶int到索引3
istore_0x3B~0x3Eint0~31istore_0 → 存储到索引0
lstore0x37long显式指定(1字节)2lstore 2 → 存储栈顶long到索引2
lstore_0x3F~0x42long0~31lstore_1 → 存储到索引1
fstore0x38float显式指定(1字节)2fstore 4 → 存储栈顶float到索引4
fstore_0x43~0x46float0~31fstore_2 → 存储到索引2
dstore0x39double显式指定(1字节)2dstore 1 → 存储栈顶double到索引1
dstore_0x47~0x4Adouble0~31dstore_3 → 存储到索引3
astore0x3A引用显式指定(1字节)2astore 0 → 存储栈顶引用到索引0
astore_0x4B~0x4E引用0~31astore_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 字节:
wide iload 256  // 0xC4 0x15 0x01 0x00(总占4字节)
  1. 数据类型差异:
    1. longdouble 类型占局部变量表的两个连续槽位(如索引 nn+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)类型操作数栈变化(执行前 → 执行后)功能描述
加法iadd0x60int…, val1, val2 → …, result弹出两个int,压入它们的和
ladd0x61long…, val1, val2 → …, result弹出两个long,压入它们的和
fadd0x62float…, val1, val2 → …, result弹出两个float,压入它们的和
dadd0x63double…, val1, val2 → …, result弹出两个double,压入它们的和
减法isub0x64int…, val1, val2 → …, result弹出两个int,压入val1 - val2
lsub0x65long…, val1, val2 → …, result弹出两个long,压入val1 - val2
fsub0x66float…, val1, val2 → …, result弹出两个float,压入val1 - val2
dsub0x67double…, val1, val2 → …, result弹出两个double,压入val1 - val2
乘法imul0x68int…, val1, val2 → …, result弹出两个int,压入它们的积
lmul0x69long…, val1, val2 → …, result弹出两个long,压入它们的积
fmul0x6Afloat…, val1, val2 → …, result弹出两个float,压入它们的积
dmul0x6Bdouble…, val1, val2 → …, result弹出两个double,压入它们的积
除法idiv0x6Cint…, val1, val2 → …, result弹出两个int,压入val1 / val2
ldiv0x6Dlong…, val1, val2 → …, result弹出两个long,压入val1 / val2
fdiv0x6Efloat…, val1, val2 → …, result弹出两个float,压入val1 / val2
ddiv0x6Fdouble…, val1, val2 → …, result弹出两个double,压入val1 / val2
求余irem0x70int…, val1, val2 → …, result弹出两个int,压入val1 % val2
lrem0x71long…, val1, val2 → …, result弹出两个long,压入val1 % val2
frem0x72float…, val1, val2 → …, result弹出两个float,压入val1 % val2
drem0x73double…, val1, val2 → …, result弹出两个double,压入val1 % val2
取反ineg0x74int…, val → …, result弹出int,压入其负值
lneg0x75long…, val → …, result弹出long,压入其负值
fneg0x76float…, val → …, result弹出float,压入其负值
dneg0x77double…, val → …, result弹出double,压入其负值
自增iinc0x84int无栈操作(直接修改局部变量)对局部变量表中的int值自增

位运算指令

类别指令操作码 (Hex)类型操作数栈变化(执行前 → 执行后)功能描述
位移ishl0x78int…, val1, val2 → …, result左移val1(按val2的二进制位数)
ishr0x7Aint…, val1, val2 → …, result算术右移(符号位填充)
iushr0x7Cint…, val1, val2 → …, result逻辑右移(零填充)
lshl0x79long…, val1, val2 → …, resultlong左移
lshr0x7Blong…, val1, val2 → …, resultlong算术右移
lushr0x7Dlong…, val1, val2 → …, resultlong逻辑右移
按位或ior0x80int…, val1, val2 → …, result弹出两个int,压入按位或结果
lor0x81long…, val1, val2 → …, result弹出两个long,压入按位或结果
按位与iand0x7Eint…, val1, val2 → …, result弹出两个int,压入按位与结果
land0x7Flong…, val1, val2 → …, result弹出两个long,压入按位与结果
按位异或ixor0x82int…, val1, val2 → …, result弹出两个int,压入按位异或结果
lxor0x83long…, val1, val2 → …, result弹出两个long,压入按位异或结果

比较指令

指令操作码 (Hex)类型操作数栈变化(执行前 → 执行后)功能描述
lcmp0x94long…, val1, val2 → …, result比较两个long值,压入结果(1 if val1 > val2, -1 if <, 0 if =)
fcmpg0x96float…, val1, val2 → …, result比较两个float值,若存在NaN则压入1(用于>或无序比较)
fcmpl0x95float…, val1, val2 → …, result比较两个float值,若存在NaN则压入-1(用于<或有序比较)
dcmpg0x98double…, val1, val2 → …, result比较两个double值,若存在NaN则压入1
dcmpl0x97double…, 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 → longi2l精确转换(long 范围更大,完全容纳 int 值)
int → floati2f可能丢失最低有效位(float 尾数位有限,超出 2^24 时无法精确表示)
int → doublei2d精确转换(double 尾数位足够容纳 int 的 32 位)
long → floatl2f可能丢失精度(float 尾数位不足,超出 2^24 时近似舍入)
long → doublel2d可能丢失精度(double 尾数位为 52,若 long 值超出 2^53 则无法精确表示)
float → doublef2d精确转换(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 → bytei2b保留低 8 位(符号位扩展)
int → shorti2s保留低 16 位(符号位扩展)
int → chari2c保留低 16 位(零扩展)
long → intl2i保留低 32 位(符号位扩展)
float → intf2i向零舍入(若结果为 NaN 或超出 int 范围,返回 0 或极值)
float → longf2l向零舍入(若结果为 NaN 或超出 long 范围,返回 0 或极值)
double → intd2i向零舍入(若结果为 NaN 或超出 int 范围,返回 0 或极值)
double → longd2l向零舍入(若结果为 NaN 或超出 long 范围,返回 0 或极值)
double → floatd2f向最接近数舍入(可能返回零、无穷大或 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 进行转化。

示例

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)操作数类型操作数栈变化(执行前 → 执行后)示例说明注意事项
new0xBB常量池索引(类的符号引用)类实例… → …, objectrefnew #1 → 创建 Object 实例仅分配内存,需后续调用 方法(如 invokespecial)初始化对象。

创建数组指令

  1. 基本类型数组
指令名称操作码 (Hex)操作数类型操作数栈变化(执行前 → 执行后)示例说明注意事项
newarray0xBC基本类型代码(1字节)基本类型数组…, size → …, arrayrefnewarray 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)操作数类型操作数栈变化(执行前 → 执行后)示例说明注意事项
anewarray0xBD常量池索引(类的符号引用)引用类型数组…, size → …, arrayrefanewarray #3 → 创建 String[]数组元素初始化为 null。

多维数组

指令名称操作码 (Hex)操作数类型操作数栈变化(执行前 → 执行后)示例说明注意事项
multianewarray0xC5常量池索引(数组类符号引用)+ 维度数(1字节)多维数组…, size1, size2… → …, arrayrefmultianewarray #5 3 → 创建三维数组维度数必须与数组类型维度匹配,各维度大小依次从栈顶弹出。

关键说明

  1. new 指令**:
    1. 仅分配对象内存,对象字段初始化为默认值(如 0null)。
    2. 需显式调用构造函数(通过 invokespecial <init>)完成初始化。
  2. 数组初始化
    1. 基本类型数组元素初始化为 0false
    2. 引用类型数组元素初始化为 null
    3. 多维数组的每个维度需单独指定大小(如 multianewarray 需从栈顶依次弹出各维大小)。
  3. 性能影响
    1. newarrayanewarray 适用于一维数组,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)操作数类型操作数栈变化(执行前 → 执行后)功能说明示例说明
getstatic0xB2常量池索引(Fieldref)静态字段… → …, value读取静态字段值并压入栈顶getstatic #8 → 加载静态变量值
putstatic0xB3常量池索引(Fieldref)静态字段…, value → …将栈顶值写入静态字段putstatic #5 → 修改静态变量值
getfield0xB4常量池索引(Fieldref)实例字段…, objectref → …, value读取对象实例字段值并压入栈顶getfield #10 → 读取对象的字段
putfield0xB5常量池索引(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)操作数栈变化(执行前 → 执行后)功能描述示例说明
instanceof0xC1…, objectref → …, result判断对象是否是某类/接口的实例,结果(1 是,0 否)压入栈顶。若对象为 null,结果为 0。instanceof java/lang/String → 判断对象是否为 String 实例。
checkcast0xC0…, 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调用静态方法,不依赖实例对象。

方法返回指令

返回类型指令名称操作数栈变化(执行前 → 执行后)关键行为
voidreturn… → …无返回值。直接退出当前方法栈帧,恢复调用者栈帧,转移控制权。
int(含 boolean、byte、char、short)ireturn…, value → …弹出栈顶 int 值,压入调用者操作数栈,丢弃当前栈其他元素。
longlreturn…, value → …弹出栈顶 long 值(占用两个栈单元),压入调用者操作数栈。
floatfreturn…, value → …弹出栈顶 float 值,压入调用者操作数栈。
doubledreturn…, value → …弹出栈顶 double 值(占用两个栈单元),压入调用者操作数栈。
引用类型(如对象、数组)areturn…, objectref → …弹出栈顶引用,压入调用者操作数栈。

关键说明

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

示例

// 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 值)操作数栈变化(执行前 → 执行后)示例场景
ifeq0x99等于 0…, value → …if (a == 0) → iload a; ifeq offset
ifne0x9A不等于 0…, value → …if (a != 0) → iload a; ifne offset
iflt0x9B小于 0…, value → …if (a < 0) → iload a; iflt offset
ifle0x9E小于等于 0…, value → …if (a <= 0) → iload a; ifle offset
ifgt0x9D大于 0…, value → …if (a > 0) → iload a; ifgt offset
ifge0x9C大于等于 0…, value → …if (a >= 0) → iload a; ifge offset
ifnull0xC6对象引用为 null…, ref → …if (obj == null) → aload obj; ifnull offset
ifnonnull0xC7对象引用不为 null…, ref → …if (obj != null) → aload obj; ifnonnull offset

关键说明

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

比较指令与条件跳转的配合

数据类型比较指令生成结果(栈顶 int 值)条件跳转示例
longlcmp1(左 > 右), 0(相等), -1(左 < 右)lcmp; ifgt offset(若左 > 右则跳转)
floatfcmpg1(左 > 右 或存在 NaN), 0(相等), -1(左 < 右)fcmpg; iflt offset(若左 < 右则跳转)
doubledcmpl1(左 > 右), 0(相等), -1(左 < 右 或存在 NaN)dcmpl; ifeq offset(若相等则跳转)

示例代码分析

int a = 10;
if (a == 0) {
    // 条件成立
}

对应字节码

iload_1       // 加载变量a到栈顶
ifeq 6        // 栈顶值等于0则跳转到偏移量6
...           // 条件不成立的代码
return

比较条件跳转指令

指令名称操作码 (Hex)比较类型操作数栈变化(执行前 → 执行后)比较规则(下部元素 vs 栈顶元素)示例场景
if_icmpeq0x9Fint(含 byte/short/char)…, a, b → …a == bif (x == y) → iload x; iload y; if_icmpeq offset
if_icmpne0xA0int…, a, b → …a != bif (x != y) → iload x; iload y; if_icmpne offset
if_icmplt0xA1int…, a, b → …a < bif (x < y) → iload x; iload y; if_icmplt offset
if_icmple0xA4int…, a, b → …a <= bif (x <= y) → iload x; iload y; if_icmple offset
if_icmpgt0xA3int…, a, b → …a > bif (x > y) → iload x; iload y; if_icmpgt offset
if_icmpge0xA2int…, a, b → …a >= bif (x >= y) → iload x; iload y; if_icmpge offset
if_acmpeq0xA5引用类型…, ref1, ref2 → …ref1 == ref2(地址相同)if (obj1 == obj2) → aload obj1; aload obj2; if_acmpeq offset
if_acmpne0xA6引用类型…, 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 等条件跳转指令。
long a = 100L, b = 200L;
if (a < b) { ... }  // 编译为:lload a; lload b; lcmp; iflt offset
  • 对象内容比较
    • 若需比较对象内容(如 String 的字符串值),需调用 equals() 方法,无法直接使用 if_acmpeq

多条件分支跳转指令

指令名称tableswitchlookupswitch
适用场景case 值连续且密集(如 1,2,3,4case 值离散或不连续(如 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 值的分布自动选择指令。例如:
// 连续值 → 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示例**
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
  1. 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)操作数操作数栈变化功能描述状态
goto0xA72 字节(有符号偏移量)无变化无条件跳转到指定偏移量(范围:-32768 ~ 32767)。主流使用
goto_w0xC84 字节(有符号偏移量)无变化无条件跳转到更大范围的偏移量(范围:-2^31 ~ 2^31-1)。主流使用
jsr0xA82 字节(有符号偏移量)… → …, returnAddress跳转到指定偏移量,并将下一条指令地址压入栈顶(用于 try-finally 的旧实现)。已废弃
jsr_w0xC94 字节(有符号偏移量)… → …, returnAddress类似 jsr,但支持更大偏移量(已废弃)。已废弃
ret0xB11 字节(局部变量索引)无变化从局部变量表中读取地址并跳转(需配合 jsr/jsr_w 使用)。已废弃

关键说明

  1. gotogoto_w
    1. goto:适用于大多数跳转场景(如循环、条件分支后的跳转),操作数范围较小。
    2. goto_w:当跳转偏移量超过 goto 的 2 字节范围时使用(如代码块非常长时)。
// 示例:循环中的 goto
while (true) {
    // 编译后:goto 偏移量(循环体结束跳转回开头)
}
  1. **废弃指令 jsr/jsr_w/ret
    1. 历史用途:用于实现 try-finally,通过 jsr 跳转到 finally 代码块,执行后通过 ret 返回。
    2. 问题:代码可读性差且易出错,现代 JVM 改用 复制 finally 代码到每个退出路径异常表 实现。
    3. 替代方案
// 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)操作数栈变化(执行前 → 执行后)功能描述示例
athrow0xBF…, exception_ref → [empty]显式抛出异常对象(throw 语句的实现)。若异常未被捕获,当前方法终止,异常传播至调用者栈帧。throw new Exception(); → new #1; dup; invokespecial ; athrow
  1. JVM 隐式抛出异常的指令示例

以下指令在检测到异常条件时会自动抛出运行时异常

指令名称异常类型触发条件示例场景
idivArithmeticException整数除法或取余运算中除数为 0int a = 10 / 0; → idiv 指令抛出异常
ldivArithmeticException长整数除法或取余运算中除数为 0long b = 100L % 0L; → ldiv 指令抛出异常
aaloadNullPointerException访问 null 引用数组的索引String[] arr = null; String s = arr[0]; → aaload 指令抛出异常
iastoreArrayIndexOutOfBoundsException数组索引越界int[] arr = new int[3]; arr[5] = 10; → iastore 指令抛出异常
checkcastClassCastException对象强制转换类型不兼容Object obj = “123”; Integer num = (Integer) obj; → checkcast 抛出异常
  1. 操作数栈的异常处理规则
场景操作数栈行为
显式抛出异常(athrow)清除当前方法的操作数栈,将异常对象压入调用者方法的操作数栈,终止当前方法执行。
隐式抛出异常清除当前方法的操作数栈,将异常对象压入调用者方法的操作数栈,终止当前方法执行。

关键说明

  1. 异常传播机制
    1. 若当前方法的异常表中未捕获异常,JVM 会依次清除当前栈帧,将异常对象传递至调用者栈帧,直到被 catch 块或默认异常处理器处理。
  2. 操作数栈清空
    1. 无论是显式还是隐式抛出异常,JVM 都会清空当前栈帧的操作数栈,确保调用者栈帧仅接收异常对象。
  3. 异常表
    1. 每个方法编译后生成异常表,定义 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

异常处理与异常表

  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 属性:
public void demo() throws IOException, SQLException { ... }
  1. 对应的字节码属性
Exceptions:
  throws java.io.IOException, java.sql.SQLException

  1. 作用
    • 供编译器和 JVM 验证方法调用是否处理了这些异常。
    • 与方法内的异常表无关,仅用于声明方法的潜在异常类型。

示例分析

1. try-catch 的字节码

public void example() {
    try {
        System.out.println("try");
    } catch (Exception e) {
        System.out.println("catch");
    }
}

异常表条目

start_pcend_pchandler_pccatch_type (常量池索引)
01013Exception

字节码逻辑

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

2. try-finally 的字节码

public void example() {
    try {
        System.out.println("try");
    } finally {
        System.out.println("finally");
    }
}

异常表条目

start_pcend_pchandler_pccatch_type
010130

实现方式

  • 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. 同步方法的字节码特征

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

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

关键点

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

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

// 同步方法示例
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. 同步代码块的字节码流程

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

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. 可重入性示例

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

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,这个解锁需要在方法退出之前完成,如果方法执行过程中出现了任何异常,将会跳到异常处理的字节码处执行相关代码,如果异常处理的字节码部分出现了问题,那就重新执行异常处理的字节码,这些内容都在异常表中写的很明确,其中异常表也在上面截图中。


http://www.kler.cn/a/576965.html

相关文章:

  • 正向代理与反向代理
  • 【玩转23种Java设计模式】结构型模式篇:组合模式
  • Python开发Scikit-learn面试题及参考答案
  • PyTorch深度学习框架60天进阶学习计划第15天:迁移学习实践
  • 【2025】基于Python+Django的酒店民宿预订管理系统(源码+调试+答疑+学习资料)
  • 基于SpringBoot的商城管理系统(源码+部署教程)
  • 使用 Yarn 安装依赖的完整指南
  • linux 基本命令教程,巡查脚本,kali镜像
  • 关于C++数据类型char的类型是整数的思考
  • 从多智能体变成一个具有通过场景生成多个决策路径 并在实施的过程中优化决策路径 openmanus 致敬开源精神中的每一个孤勇者
  • HeapDumpBeforeFullGC和HeapDumpOnOutOfMemoryError区别
  • 【机器学习中的“模型穿越”问题:定义、解决方法】
  • 《探秘课程蒸馏体系“三阶训练法”:解锁知识层级递进式迁移的密码》
  • Vue3技术实践:基于XLSX与File-Saver的Excel高效导出方案
  • 《Linux C 智能 IO 矩阵:输入输出的自适应数据流转》
  • sdp与传统网络安全防护的区别 sdn 网络安全
  • iOS侧滑返回手势冲突处理
  • “Predict”和“Foresee”的区别
  • Windows 虚拟化架构解析:WSL 与 Hyper-V 及其对 Docker 部署的影响
  • 数据库事务的 ACID,通过MVCC能做什么