程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • Java底层 - JVM

    • JVM - Java体系结构
    • JVM - 类加载子系统
    • JVM - 运行时数据区概述及线程
    • JVM - 程序计数器
    • JVM - 虚拟机栈
    • JVM - 本地方法接口
    • JVM - 本地方法栈
    • JVM - 堆 (Heap)
    • JVM - 方法区
    • JVM - 对象实例化内存布局
    • JVM - 直接内存管理
    • JVM - 执行引擎
    • JVM - 字符串常量池 (StringTable)
    • JVM - 垃圾回收概述
    • JVM - 垃圾回收相关算法
    • JVM - 垃圾回收相关概念
    • JVM - 垃圾回收器
    • JVM - Class文件结构
    • JVM - 字节码指令集与解析
      • 1. 概述:字节码指令的基础
        • 1.1 字节码:虚拟机的“汇编语言”
        • 1.2 指令结构:操作码与操作数
        • 1.3 学习字节码的价值
        • 1.4 简化的 JVM 执行模型
        • 1.5 字节码与数据类型
        • 1.6 指令分类
      • 2. 加载与存储指令
        • 2.1 常用指令概览
        • 2.2 操作数栈与局部变量表再探
        • 2.3 局部变量压栈指令详解
        • 2.4 常量入栈指令详解
        • 2.5 出栈装入局部变量表指令详解
      • 3. 算术指令
        • 3.1 分类与类型支持
        • 3.2 运算溢出
        • 3.3 舍入模式
        • 3.4 所有算术指令列表
        • 3.5 算术指令代码示例
        • 3.6 比较指令详解 (lcmp, fcmp*, dcmp*)
      • 4. 类型转换指令
        • 4.1 类型转换指令集概览
        • 4.2 宽化类型转换 (Widening Conversion)
        • 4.3 窄化类型转换 (Narrowing Conversion)
      • 5. 对象的创建与访问指令
        • 5.1 创建指令
        • 5.2 字段访问指令
        • 5.3 数组操作指令
        • 5.4 类型检查指令
      • 6. 方法调用与返回指令
        • 6.1 方法调用指令
        • 6.2 方法返回指令
      • 7. 操作数栈管理指令
      • 8. 控制转移指令
        • 8.1 比较指令 (lcmp, fcmp*, dcmp*)
        • 8.2 条件跳转指令 (if*)
        • 8.3 比较条件跳转指令 (if_icmp*, if_acmp*)
        • 8.4 多条件分支跳转 (tableswitch, lookupswitch)
        • 8.5 无条件跳转 (goto, goto_w)
      • 9. 异常处理指令
        • 9.1 抛出异常指令 (athrow)
        • 9.2 异常处理与异常表 (Exception Table)
      • 10. 同步控制指令 (monitorenter, monitorexit)
        • 10.1 同步机制概述
        • 10.2 方法级同步的实现
        • 10.3 代码块同步的实现
    • JVM - 类的加载过程详解
    • JVM - 再谈类的加载器
    • JVM - 调优概述
    • JVM - 监控及诊断工具cmd
    • JVM - 监控及诊断工具GUI
    • JVM - 运行时参数
    • JVM - 分析GC日志
  • Java底层
  • Java底层 - JVM
scholar
2024-01-28
目录

JVM - 字节码指令集与解析

# 1. 概述:字节码指令的基础

官方文档参考: JVMS 8 - Chapter 6: The Java Virtual Machine Instruction Set (opens new window)

# 1.1 字节码:虚拟机的“汇编语言”

Java 字节码(Bytecode)是 Java 虚拟机 (JVM) 执行的指令集。可以将其类比为物理计算机的汇编语言,是 JVM 能够理解和执行的最底层、最基本的命令。

# 1.2 指令结构:操作码与操作数

  • 基本构成: JVM 指令由一个单字节长度的操作码 (Opcode) 和零至多个操作数 (Operands) 构成。
  • 操作码 (Opcode): 代表一种特定的操作含义,是一个 0 到 255 之间的数字。由于长度限制为 1 字节,JVM 的指令总数不超过 256 条。
  • 操作数 (Operands): 跟随在操作码之后,提供该操作所需的参数。
  • 基于操作数栈: JVM 采用面向操作数栈的架构,而不是基于寄存器的架构。这意味着大多数指令直接在操作数栈上工作(弹栈、运算、压栈),因此很多指令只有一个操作码,没有显式的操作数。操作数通常隐含在指令本身或从操作数栈中获取。

# 1.3 学习字节码的价值

熟悉 JVM 指令对于以下场景至关重要:

  • 动态字节码生成: 使用 ASM、Javassist 等库在运行时创建或修改类。
  • 反编译 Class 文件: 理解 javap 或其他反编译工具的输出。
  • Class 文件修复与分析: 对字节码进行底层分析和修改。
  • 深入理解 JVM: 理解 Java 代码的底层执行逻辑、性能瓶颈。

因此,阅读和理解字节码是掌握 Java 虚拟机的基础技能。

# 1.4 简化的 JVM 执行模型

如果不考虑复杂的异常处理和 JIT 编译优化,JVM 解释器的基本执行模型可以简化为以下循环:

do {
    // 1. 自动递增程序计数器 (PC Register)
    pc++;

    // 2. 根据 PC 指示的位置,从字节码流中取出当前指令的操作码
    opcode = bytecode_stream[pc];

    // 3. 如果指令有操作数,则从字节码流中取出操作数
    if (opcode_has_operands(opcode)) {
        operands = bytecode_stream[pc+1 ...];
        pc += length_of_operands; // 更新 PC 跳过操作数
    }

    // 4. 执行操作码定义的操作(可能会修改操作数栈、局部变量表等)
    execute_operation(opcode, operands);

} while (more_bytecode_exists());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 1.5 字节码与数据类型

JVM 指令集的设计与 Java 的基本数据类型紧密相关。

  • 类型指示符: 大多数指令的操作码助记符 (Mnemonic) 中包含了表明其操作数据类型的特殊字符:
    • i: int
    • l: long
    • s: short
    • b: byte
    • c: char
    • f: float
    • d: double
    • a: reference (对象引用)
    • 例如:iload (加载 int), fstore (存储 float), ladd (long 型加法), areturn (返回引用)。
  • 无类型指示符:
    • 某些指令(如 arraylength)的操作数类型是固定的(如数组引用),无需类型指示符。
    • 某些指令(如无条件跳转 goto)与数据类型无关。
  • 对 byte, char, short, boolean 的处理:
    • JVM 指令集中没有直接支持 boolean, byte, char, short 类型的专用算术、加载、存储指令(除了特定的数组操作指令)。
    • 编译器处理: 在编译期或运行期,这些类型会被扩展 (Sign-Extend 或 Zero-Extend) 为相应的 int 类型进行处理。
      • byte, short -> int (带符号扩展)
      • boolean, char -> int (零位扩展,boolean true=1, false=0)
    • 运算: 对这些类型的运算实际上是使用相应的 int 类型指令完成的。
    • 数组: 处理 boolean[], byte[], char[], short[] 数组时,也使用 int 类型的相关字节码指令(如 baload 对应 byte/boolean 数组加载,但内部可能按 int 处理)。

# 1.6 指令分类

为了便于学习,JVM 字节码指令可以按用途大致分为 9 类:

  1. 加载与存储指令 (Load and Store Instructions): 在局部变量表和操作数栈之间传输数据。
  2. 算术指令 (Arithmetic Instructions): 执行数值运算。
  3. 类型转换指令 (Type Conversion Instructions): 在不同数值类型间转换。
  4. 对象的创建与访问指令 (Object Creation and Manipulation Instructions): 创建对象/数组,访问字段/数组元素。
  5. 方法调用与返回指令 (Method Invocation and Return Instructions): 调用方法,从方法返回。
  6. 操作数栈管理指令 (Operand Stack Management Instructions): 直接操作操作数栈(如出栈、复制、交换)。
  7. 控制转移指令 (Control Transfer Instructions): 条件/无条件跳转,switch 语句。
  8. 异常处理指令 (Exception Handling Instructions): 抛出异常 (athrow)。异常捕获由异常表处理。
  9. 同步控制指令 (Synchronization Instructions): 支持 synchronized 关键字 (monitorenter, monitorexit)。

核心交互:

  • 指令可以从多个来源获取数据(局部变量表、常量池、堆对象、方法调用结果等),并将数据压入操作数栈。
  • 指令也可以从操作数栈弹出数据,用于赋值给局部变量、进行运算、作为方法参数、执行系统调用等。

# 2. 加载与存储指令

这类指令负责在栈帧的局部变量表 (Local Variable Table) 和操作数栈 (Operand Stack) 之间来回传递数据。

# 2.1 常用指令概览

  • 局部变量 -> 操作数栈 (加载 Load):
    • xload index: 从指定索引 index 的局部变量加载。(iload, lload, fload, dload, aload)
    • xload_<n>: 从索引 0 到 3 的局部变量加载,n 隐含在指令中。(iload_0, lload_1, fload_2, dload_3, aload_0 等)
    • xaload: 从数组加载元素到操作数栈。(baload, caload, saload, iaload, laload, faload, daload, aaload)
  • 常量 -> 操作数栈 (加载 Load Constant):
    • bipush byte: 将单字节常量 (-128~127) 推送入栈。
    • sipush short: 将双字节常量 (-32768~32767) 推送入栈。
    • ldc index: 从常量池加载 int, float, String 或 Class 引用 (单字节索引)。
    • ldc_w index: 功能同 ldc,使用双字节索引。
    • ldc2_w index: 从常量池加载 long 或 double (双字节索引)。
    • aconst_null: 将 null 推送入栈。
    • iconst_m1, iconst_<i>: 将 int 型 -1, 0, 1, 2, 3, 4, 5 推送入栈。
    • lconst_<l>: 将 long 型 0, 1 推送入栈。
    • fconst_<f>: 将 float 型 0, 1, 2 推送入栈。
    • dconst_<d>: 将 double 型 0, 1 推送入栈。
  • 操作数栈 -> 局部变量表 (存储 Store):
    • xstore index: 将栈顶元素存储到指定索引 index 的局部变量。(istore, lstore, fstore, dstore, astore)
    • xstore_<n>: 将栈顶元素存储到索引 0 到 3 的局部变量。(istore_0, lstore_1, fstore_2, dstore_3, astore_0 等)
    • xastore: 将栈顶元素存储到数组的指定索引位置。(bastore, castore, sastore, iastore, lastore, fastore, dastore, aastore)
  • 局部变量表索引扩展:
    • wide: 用于扩展局部变量索引或 iinc 指令的操作数,使其支持大于 255 的索引或增量。

xload_<n> 和 xstore_<n> 的说明: 这些是特定索引(0-3)的优化指令。它们的操作数(局部变量索引)隐含在操作码中,无需额外读取操作数,执行更快,字节码更紧凑。语义上 iload_1 等同于 iload 1。

# 2.2 操作数栈与局部变量表再探

  • 操作数栈 (Operand Stack):
    • 每个方法执行时在栈帧内创建。
    • 是一个后进先出 (LIFO) 的栈。
    • 用于存放方法执行过程中的操作数和中间结果。
    • 指令执行前,所需操作数压栈;执行时,操作数弹栈;执行后,结果压栈。
    • 栈的深度在编译期确定,记录在 Code 属性的 max_stack 中。 操作数栈示例-iadd前 操作数栈示例-iadd后
  • 局部变量表 (Local Variable Table):
    • 每个方法执行时在栈帧内创建。
    • 本质是一个数组,用于存储方法参数和方法体内定义的局部变量。
    • 容量以槽 (Slot) 为单位。boolean, byte, char, short, int, float, reference 类型占用 1 个 Slot;long 和 double 类型占用 2 个连续的 Slot。
    • 索引:
      • 对于实例方法 (非 static),第 0 个 Slot 固定存储当前对象的引用 this。
      • 之后依次存放方法参数。
      • 再之后存放方法体内的局部变量。
    • Slot 复用: 方法内作用域不重叠的局部变量可能会复用同一个 Slot,以节省空间。
    • 局部变量表的容量在编译期确定,记录在 Code 属性的 max_locals 中。
    • 局部变量表是 GC Roots 的重要组成部分。

示例: 局部变量表布局

// 文件名: LocalVarExample.java
public class LocalVarExample {
    public void foo(long l, float f) { // 实例方法
        // Slot 0: this (类型 LocalVarExample)
        // Slot 1, 2: l (long 类型,占两个槽)
        // Slot 3: f (float 类型)
        {
            int i = 0; // Slot 4: i (int 类型)
        } // i 的作用域结束,Slot 4 可被复用
        {
            String s = "Hello"; // Slot 4: s (String 引用类型),复用了 Slot 4
        } // s 的作用域结束
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

局部变量表示意图 局部变量表示例图

# 2.3 局部变量压栈指令详解

将局部变量表中的值加载到操作数栈顶。

  • xload_<n>: (x=i, l, f, d, a; n=0, 1, 2, 3)
    • iload_0: 加载第 0 个 Slot 的 int 值。
    • aload_0: 加载第 0 个 Slot 的引用值 (通常是 this)。
    • lload_1: 加载第 1, 2 个 Slot 的 long 值。
  • xload index: (x=i, l, f, d, a)
    • iload 4: 加载第 4 个 Slot 的 int 值。
    • aload 5: 加载第 5 个 Slot 的引用值。

代码示例:

// 文件名: LoadTest.java
public class LoadTest {
    // 参数在局部变量表中的布局:
    // Slot 0: this
    // Slot 1: num (int)
    // Slot 2: obj (Object ref)
    // Slot 3, 4: count (long)
    // Slot 5: flag (boolean -> int)
    // Slot 6: arr (short[] ref)
    public void load(int num, Object obj, long count, boolean flag, short[] arr) {
        // 访问 num (Slot 1)
        System.out.println(num);
        // 访问 obj (Slot 2)
        System.out.println(obj);
        // 访问 count (Slot 3)
        System.out.println(count);
        // 访问 flag (Slot 5)
        System.out.println(flag);
        // 访问 arr (Slot 6)
        System.out.println(arr);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

对应字节码 (javap -v LoadTest.class):

// ... 省略 System.out 获取 ...
   // System.out.println(num);
   3: iload_1       // 加载 Slot 1 (num) 的 int 值到操作数栈
   // ... 调用 println(I) ...

   // System.out.println(obj);
   7: aload_2       // 加载 Slot 2 (obj) 的引用值到操作数栈
   // ... 调用 println(Ljava/lang/Object;) ...

   // System.out.println(count);
  11: lload_3       // 加载 Slot 3, 4 (count) 的 long 值到操作数栈
  // ... 调用 println(J) ...

  // System.out.println(flag);
  15: iload         5 // 加载 Slot 5 (flag) 的 int 值 (boolean 视为 int) 到操作数栈
  // ... 调用 println(Z) ... (println 对 boolean 有重载)

  // System.out.println(arr);
  19: aload         6 // 加载 Slot 6 (arr) 的引用值到操作数栈
  // ... 调用 println(Ljava/lang/Object;) ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

LoadTest 字节码截图

# 2.4 常量入栈指令详解

将常量值推送到操作数栈顶。

  • const 系列: 用于加载特定的小范围常量或 null,指令本身隐含了常量值。
    • iconst_m1 (-1), iconst_0 (0), ..., iconst_5 (5)
    • lconst_0 (0L), lconst_1 (1L)
    • fconst_0 (0.0f), fconst_1 (1.0f), fconst_2 (2.0f)
    • dconst_0 (0.0d), dconst_1 (1.0d)
    • aconst_null (null)
  • push 系列: 用于加载更大范围的整数常量。
    • bipush byte: 加载一个字节表示的 int (-128 ~ 127)。操作数是 1 字节。
    • sipush short: 加载一个短整型表示的 int (-32768 ~ 32767)。操作数是 2 字节。
  • ldc 系列: 从常量池加载。
    • ldc index: 加载常量池中索引为 index (1 字节) 的项,可以是 int, float, String (字面量引用), Class 引用, MethodType, MethodHandle。
    • ldc_w index: 功能同 ldc,但索引 index 是 2 字节,支持更大的常量池。
    • ldc2_w index: 加载常量池中索引为 index (2 字节) 的 long 或 double 值。

常量加载指令选择优先级 (以 int 为例):

  1. -1 到 5: iconst_<i>
  2. -128 到 127: bipush
  3. -32768 到 32767: sipush
  4. 其他 int 值: ldc (值存放在常量池)

代码示例 1: int 常量加载

// 文件名: PushConstLdcTest.java
public class PushConstLdcTest {
    public void pushConstLdc() {
        int i = -1;     // iconst_m1
        int a = 5;      // iconst_5
        int b = 6;      // bipush 6
        int c = 127;    // bipush 127
        int d = 128;    // sipush 128
        int e = 32767;  // sipush 32767
        int f = 32768;  // ldc (从常量池加载)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

对应字节码:

   0: iconst_m1
   1: istore_1       // i = -1
   2: iconst_5
   3: istore_2       // a = 5
   4: bipush        6
   6: istore_3       // b = 6
   7: bipush        127
   9: istore        4 // c = 127
  11: sipush        128
  14: istore        5 // d = 128
  16: sipush        32767
  19: istore        6 // e = 32767
  21: ldc           #2 // 从常量池加载 int 32768 (假设 #2 是 Integer 32768)
  23: istore        7 // f = 32768
  25: return
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

PushConstLdcTest 字节码截图

代码示例 2: 其他类型常量加载

// 文件名: ConstLdcTest.java
import java.util.Date;

public class ConstLdcTest {
    public void constLdc() {
        long a1 = 1L;   // lconst_1
        long a2 = 2L;   // ldc2_w (从常量池加载 long 2)

        float b1 = 2f;  // fconst_2
        float b2 = 3f;  // ldc (从常量池加载 float 3.0)

        double c1 = 1d; // dconst_1
        double c2 = 2d; // ldc2_w (从常量池加载 double 2.0)

        Date d = null;  // aconst_null
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

对应字节码:

   0: lconst_1
   1: lstore_1      // a1 = 1L
   2: ldc2_w        #2 // lstore_3 (假设 #2 是 Long 2)
   5: lstore_3      // a2 = 2L
   6: fconst_2
   7: fstore        5 // b1 = 2f
   9: ldc           #4 // fstore 6 (假设 #4 是 Float 3.0f)
  11: fstore        6 // b2 = 3f
  13: dconst_1
  14: dstore        7 // c1 = 1d
  16: ldc2_w        #5 // dstore 9 (假设 #5 是 Double 2.0d)
  19: dstore        9 // c2 = 2d
  21: aconst_null
  22: astore        11 // d = null
  24: return
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

ConstLdcTest 字节码截图

常量指令总结表:

类型 常量值范围 指令
int/byte/char/short/boolean -1 到 5 iconst_<i>
-128 到 127 bipush
-32768 到 32767 sipush
其他 int ldc/ldc_w
long 0, 1 lconst_<l>
其他 long ldc2_w
float 0.0, 1.0, 2.0 fconst_<f>
其他 float ldc/ldc_w
double 0.0, 1.0 dconst_<d>
其他 double ldc2_w
reference null aconst_null
String 字面量 ldc/ldc_w
Class 字面量 ldc/ldc_w

# 2.5 出栈装入局部变量表指令详解

将操作数栈顶的一个或两个元素弹出,并存储到局部变量表的指定 Slot 中。常用于赋值操作。

  • xstore_<n>: (x=i, l, f, d, a; n=0, 1, 2, 3)
    • istore_1: 将栈顶 int 存入 Slot 1。
    • astore_0: 将栈顶引用存入 Slot 0 (通常是 this 的赋值)。
    • dstore_2: 将栈顶 double (占两个 Slot) 存入 Slot 2 和 3。
  • xstore index: (x=i, l, f, d, a)
    • istore 4: 将栈顶 int 存入 Slot 4。
    • lstore 5: 将栈顶 long 存入 Slot 5 和 6。
  • xastore: 将操作数栈顶的值存储到数组的指定索引位置。
    • 执行前栈结构 (从栈顶往下): value, index, arrayref
    • 执行后: 栈顶三个元素弹出,arrayref[index] = value。

代码示例:

// 文件名: StoreTest.java
public class StoreTest {
    // 参数 k 在 Slot 1 (int), d 在 Slot 2, 3 (double)
    public void store(int k, double d) {
        // Slot 4: m (int)
        int m = k + 2;
        // Slot 5, 6: l (long)
        long l = 12L;
        // Slot 7: str (String ref)
        String str = "kele";
        // Slot 8: f (float)
        float f = 10.0F;
        // 重新给参数 d 赋值 (Slot 2, 3)
        d = 10.0;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

对应字节码:

   // int m = k + 2;
   0: iload_1       // 加载 k (Slot 1)
   1: iconst_2      // 加载常量 2
   2: iadd          // k + 2
   3: istore        4 // 将结果存入 m (Slot 4)

   // long l = 12L;
   5: ldc2_w        #2 // 加载 long 12L (假设 #2 是 Long 12)
   8: lstore        5 // 将结果存入 l (Slot 5, 6)

   // String str = "kele";
  10: ldc           #4 // 加载 "kele" (假设 #4 是 String "kele")
  12: astore        7 // 将引用存入 str (Slot 7)

  // float f = 10.0F;
  14: ldc           #5 // 加载 10.0f (假设 #5 是 Float 10.0f)
  16: fstore        8 // 将结果存入 f (Slot 8)

  // d = 10.0;
  18: ldc2_w        #6 // 加载 10.0d (假设 #6 是 Double 10.0d)
  21: dstore_2      // 将结果存入 d (Slot 2, 3)
  22: return
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

StoreTest 字节码截图

# 3. 算术指令

用于对操作数栈上的两个数值进行运算,并将结果压回操作数栈。

# 3.1 分类与类型支持

  • 两大类: 整数运算指令 和 浮点数运算指令。
  • 整数运算:
    • 针对 int 和 long 类型有专用指令 (如 iadd, ladd)。
    • 不支持 直接对 byte, short, char, boolean 的运算。这些类型在运算前会被提升 (Promote) 为 int,然后使用 int 的算术指令进行运算。 类型提升示意图
  • 浮点数运算:
    • 针对 float 和 double 类型有专用指令 (如 fadd, dadd)。
    • 遵循 IEEE 754 标准。
    • 运算结果可能产生 Infinity (无穷大) 或 NaN (Not a Number)。

# 3.2 运算溢出

  • 整数运算:
    • 除了除法 (idiv, ldiv) 和求余 (irem, lrem) 在除数为 0 时会抛出 ArithmeticException 外,其他整数运算(加减乘、位移)即使发生溢出(结果超出类型表示范围),也不会抛出异常,结果会进行环绕 (Wrap-around),例如 Integer.MAX_VALUE + 1 结果为 Integer.MIN_VALUE。
  • 浮点数运算:
    • 溢出通常产生有符号的无穷大 (Infinity 或 -Infinity)。
    • 某些未定义的操作(如 0.0 / 0.0, sqrt(-1.0)) 产生 NaN。
    • 任何涉及 NaN 的运算,结果都是 NaN。
    • 浮点运算不会抛出运行时异常 (如 ArithmeticException)。

# 3.3 舍入模式

  • 向最接近数舍入 (Round to Nearest): JVM 进行浮点数运算时,非精确结果必须舍入到最接近的可表示值。如果距离两个可表示值相等,则选择最低有效位为 0 的那个(这符合 IEEE 754 的默认舍入模式)。
  • 向零舍入 (Round towards Zero): 将浮点数转换为整数时(如 f2i, d2l),采用此模式。结果是绝对值不大于原浮点数的最接近整数。

代码示例:浮点数特殊值

// 文件名: FloatSpecialValues.java
public class FloatSpecialValues {
    public void method() {
        int i = 10;
        // int / 0.0 -> Infinity (除数是浮点数 0.0)
        double j = i / 0.0;
        System.out.println(j); // 输出 Infinity

        double d1 = 0.0;
        // 0.0 / 0.0 -> NaN
        double d2 = d1 / 0.0;
        System.out.println(d2); // 输出 NaN
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 3.4 所有算术指令列表

运算类别 操作 int (byte,short,char,bool) long float double
加法 + iadd ladd fadd dadd
减法 - isub lsub fsub dsub
乘法 * imul lmul fmul dmul
除法 / idiv ldiv fdiv ddiv
求余/取模 % irem lrem frem drem
取反/取负 - (一元) ineg lneg fneg dneg
自增 += c iinc index by const - - -
位运算 左移 << ishl lshl - -
右移 >> ishr (算术) lshr - -
无符号右移>>> iushr lushr - -
按位或 \| ior lor - -
按位与 & iand land - -
按位异或 ^ ixor lxor - -
比较 (非布尔结果) 比较 - lcmp fcmpg/fcmpl dcmpg/dcmpl

说明:

  • 比较指令 (lcmp, fcmp*, dcmp*) 的结果不是 boolean,而是 int 值 (-1, 0, 或 1),通常用于后续的条件跳转。
  • iinc 指令比较特殊,它直接修改局部变量表中的值,而不是操作数栈。iinc index by const 表示将索引为 index 的局部变量增加 const (一个带符号字节)。

# 3.5 算术指令代码示例

示例 1: 取反指令 (fneg)

// 文件名: NegationTest.java
public class NegationTest {
    public void method() {
        float i = 10f;  // ldc, fstore_1
        float j = -i;   // fload_1, fneg, fstore_2
        i = -j;         // fload_2, fneg, fstore_1
    }
}
1
2
3
4
5
6
7
8

NegationTest 字节码截图

示例 2: 加法指令 (fadd)

// 文件名: AddTest.java
public class AddTest {
    public void method() {
        float i = 100f; // ldc, fstore_1
        i = i + 10f;   // fload_1, ldc, fadd, fstore_1
    }
}
1
2
3
4
5
6
7

AddTest 字节码截图

示例 3: 自增指令 (iinc)

// 文件名: IncrementTest.java
public class IncrementTest {
    public void method() {
        int i = 100; // bipush, istore_1
        i += 10;    // iinc 1 by 10 (直接修改 Slot 1 的值,效率更高)
    }
}
1
2
3
4
5
6
7

IncrementTest 字节码截图 对比示例 2,iinc 指令更简洁高效。

示例 4: 复合运算

// 文件名: CompoundCalc.java
public class CompoundCalc {
    public int method() {
        int a = 80; // bipush 80, istore_1
        int b = 7;  // bipush 7, istore_2
        int c = 10; // bipush 10, istore_3
        // (a + b) * c
        return (a + b) * c;
        // iload_1 (a)
        // iload_2 (b)
        // iadd    (a+b)
        // iload_3 (c)
        // imul    ((a+b)*c)
        // ireturn
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

CompoundCalc 字节码截图

示例 5: 位运算

// 文件名: BitwiseOp.java
public class BitwiseOp {
    // 参数 i 在 Slot 1, j 在 Slot 2
    public int method(int i, int j) {
        // (i + j - 1) & ~(j - 1)
        return ((i + j - 1) & ~(j - 1));
        // iload_1 (i)
        // iload_2 (j)
        // iadd    (i+j)
        // iconst_1
        // isub    (i+j-1)
        // iload_2 (j)
        // iconst_1
        // isub    (j-1)
        // iconst_m1 (-1, 用于按位取反)
        // ixor    (~(j-1), 因为 ~x 等价于 x ^ -1)
        // iand    ((i+j-1) & ~(j-1))
        // ireturn
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

BitwiseOp 字节码截图

示例 6: 混合运算顺序

// 文件名: MixedOrder.java
public class MixedOrder {
    public static int bar(int i) { // i 在 Slot 0 (静态方法无 this)
      // ((i + 1) - 2) * 3 / 4
      return ((i + 1) - 2) * 3 / 4;
      // iload_0 (i)
      // iconst_1
      // iadd    (i+1)
      // iconst_2
      // isub    ((i+1)-2)
      // iconst_3
      // imul    (((i+1)-2)*3)
      // iconst_4
      // idiv    ((((i+1)-2)*3)/4)
      // ireturn
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

MixedOrder 字节码截图

示例 7: byte 类型运算 (提升为 int)

// 文件名: ByteAdd.java
public class ByteAdd {
    public void add() {
        byte i = 15; // bipush 15, istore_1 (存入 int Slot)
        int j = 8;   // bipush 8, istore_2
        // i + j (实际是 int + int)
        int k = i + j;
        // iload_1 (加载 i, 已是 int)
        // iload_2 (加载 j)
        // iadd
        // istore_3 (存入 k)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

ByteAdd 字节码截图

示例 8: i++ vs ++i

// 文件名: IncrementCompare.java
public class IncrementCompare {
    public void method() {
        int i = 10;     // bipush 10, istore_1
        // a = i++ (先用 i 的旧值,i 再自增)
        int a = i++;
        // iload_1 (加载 i=10)
        // iinc 1 by 1 (i 变为 11)
        // istore_2 (将旧值 10 存入 a)

        int j = 20;     // bipush 20, istore_3
        // b = ++j (j 先自增,再用 j 的新值)
        int b = ++j;
        // iinc 3 by 1 (j 变为 21)
        // iload_3 (加载 j=21)
        // istore 4 (将新值 21 存入 b)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

IncrementCompare 字节码截图

示例 9: i = i++ 的陷阱

// 文件名: IncrementTrap.java
public class IncrementTrap {
    public void method() {
        int i = 10; // bipush 10, istore_1
        // i = i++;
        // 1. 加载 i 的当前值 (10) 到操作数栈
        //    iload_1
        // 2. 将局部变量表中的 i 自增 1 (i 变为 11)
        //    iinc 1 by 1
        // 3. 将操作数栈顶的值 (旧值 10) 存回局部变量 i
        //    istore_1 (覆盖了自增后的 11)
        i = i++;
        System.out.println(i); // 输出 10
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

IncrementTrap 字节码截图

# 3.6 比较指令详解 (lcmp, fcmp*, dcmp*)

  • 作用: 比较操作数栈顶的两个数值(long, float, double),并将比较结果(-1, 0, 或 1)压入栈。
  • 指令:
    • lcmp: 比较两个 long。
    • fcmp<op>: 比较两个 float (fcmpg, fcmpl)。
    • dcmp<op>: 比较两个 double (dcmpg, dcmpl)。
  • 操作数: 从栈顶弹出两个值进行比较(假设栈顶是 value2,次顶是 value1)。
  • 结果压栈:
    • 若 value1 > value2, 压入 1。
    • 若 value1 == value2, 压入 0。
    • 若 value1 < value2, 压入 -1。
  • 处理 NaN:
    • fcmp* 和 dcmp* 需要处理 NaN。
    • fcmp**g** / dcmp**g** (g for greater): 如果任一操作数是 NaN,压入 1。
    • fcmp**l** / dcmp**l** (l for less): 如果任一操作数是 NaN,压入 -1。
  • 后续: 比较指令的结果通常立即被条件跳转指令(如 ifeq, iflt 等)使用。

# 4. 类型转换指令

用于在不同的数值类型之间进行转换。

# 4.1 类型转换指令集概览

JVM 提供了多种数值类型转换指令。基本格式是 <source_type>2<target_type> (读作 source to target)。

源类型 转 byte 转 char 转 short 转 int 转 long 转 float 转 double
int i2b i2c i2s - i2l i2f i2d
long (l2i,i2b) (l2i,i2c) (l2i,i2s) l2i - l2f l2d
float (f2i,i2b) (f2i,i2c) (f2i,i2s) f2i f2l - f2d
double (d2i,i2b) (d2i,i2c) (d2i,i2s) d2i d2l d2f -

注: 括号内表示需要通过中间类型 (int) 进行转换。

# 4.2 宽化类型转换 (Widening Conversion)

从小范围类型向大范围类型的转换,通常是安全的,但也可能损失精度。

  • JVM 直接支持 (无需指令):
    • byte -> int
    • short -> int
    • char -> int
    • (这些转换在加载或运算时自动完成)
  • JVM 提供指令支持:
    • int -> long (i2l)
    • int -> float (i2f)
    • int -> double (i2d)
    • long -> float (l2f)
    • long -> double (l2d)
    • float -> double (f2d)

精度损失:

  • int -> long, int -> double, long -> double: 不损失信息。
  • int -> float, long -> float, long -> double: 可能损失精度 (丢失低位有效数字),但数量级不会错。转换遵循 IEEE 754 向最接近数舍入。
  • 宽化转换永远不会导致运行时异常。

代码示例 1: 常见宽化转换

// 文件名: WideningTest1.java
public class WideningTest1 {
    public void upCast1() {
        int i = 10;
        long l = i;     // 隐式转换, 字节码使用 i2l
        float f = i;    // 隐式转换, 字节码使用 i2f
        double d = i;   // 隐式转换, 字节码使用 i2d

        long l2 = 100L;
        float f1 = l2;  // 隐式转换, 字节码使用 l2f
        double d1 = l2; // 隐式转换, 字节码使用 l2d

        float f2 = 10.0f;
        double d2 = f2; // 隐式转换, 字节码使用 f2d
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

WideningTest1 字节码截图

代码示例 2: 宽化转换精度损失

// 文件名: WideningTest2.java
public class WideningTest2 {
    public void upCast2() {
        int i = 123123123;
        float f = i; // 可能损失精度
        System.out.println(f); // 输出: 1.2312312E8 (精度丢失)

        long l = 123123123123123123L;
        double d = l; // 可能损失精度
        System.out.println(d); // 输出: 1.2312312312312312E17 (精度丢失)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

代码示例 3: byte/short 到 int (无显式指令)

// 文件名: WideningTest3.java
public class WideningTest3 {
    // 参数 b 在 Slot 1, s 在 Slot 2
    public void upCast3(byte b, short s) {
        // byte b 赋值给 int i 时,JVM 直接处理,无需 i2b 指令
        int i = b; // iload_1, istore_3
        // short s 赋值给 int i 时,JVM 直接处理,无需 i2s 指令
        i = s;     // iload_2, istore_3
    }
}
// 字节码中不会出现 i2b 或 i2s 用于这种赋值。
1
2
3
4
5
6
7
8
9
10
11

# 4.3 窄化类型转换 (Narrowing Conversion)

从大范围类型向小范围类型的转换,可能导致信息丢失(精度、范围、符号)。需要显式强制类型转换。

  • JVM 提供指令支持:
    • int -> byte (i2b)
    • int -> char (i2c)
    • int -> short (i2s)
    • long -> int (l2i)
    • float -> int (f2i)
    • float -> long (f2l)
    • double -> int (d2i)
    • double -> long (d2l)
    • double -> float (d2f)

精度与范围损失:

  • 整数间窄化: 高位被截断。例如 int 转 byte 只保留最低 8 位,可能导致符号改变和值环绕。
  • 浮点转整数:
    • NaN -> 0。
    • Infinity / -Infinity -> 对应整数类型的 MAX_VALUE / MIN_VALUE。
    • 普通浮点数 -> 使用向零舍入得到整数。如果结果在目标整数类型范围内,则为该整数;否则为目标类型的 MAX_VALUE 或 MIN_VALUE。
  • double -> float:
    • 可能损失精度。
    • 绝对值过大 -> Infinity / -Infinity。
    • 绝对值过小 -> 0.0 / -0.0。
    • NaN -> NaN。
  • 窄化转换永远不会导致 JVM 抛出运行时异常。

代码示例 1: 常见窄化转换

// 文件名: NarrowingTest1.java
public class NarrowingTest1 {
    public void downCast1() {
        int i = 10;
        byte b = (byte)i;   // i2b
        short s = (short)i; // i2s
        char c = (char)i;   // i2c

        long l = 10L;
        int i1 = (int)l;    // l2i
        // long 转 byte 需要先转 int: l -> i -> b
        byte b1 = (byte)l;  // l2i, i2b
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

NarrowingTest1 字节码截图 注意 byte b1 = (byte)l; 对应的字节码是 l2i 后跟 i2b。

代码示例 2: 窄化转换值环绕

// 文件名: NarrowingTest2.java
public class NarrowingTest2 {
    public void downCast2() {
        int i = 128; // 1000 0000 (二进制)
        // 转换为 byte 时,只保留低 8 位,即 1000 0000
        // byte 的 1000 0000 表示 -128 (补码)
        byte b = (byte)i; // i2b
        System.out.println(b); // 输出 -128
    }
}
1
2
3
4
5
6
7
8
9
10

代码示例 3: 浮点数窄化转换特殊值

// 文件名: NarrowingTest3.java
public class NarrowingTest3 {
    public void downCast3() {
        double d_nan = Double.NaN;
        int i_nan = (int)d_nan; // d2i
        System.out.println(d_nan); // 输出 NaN
        System.out.println(i_nan); // 输出 0

        double d_inf = Double.POSITIVE_INFINITY;
        long l_inf = (long)d_inf; // d2l
        int i_inf = (int)d_inf;   // d2i
        System.out.println(l_inf); // 输出 Long.MAX_VALUE (9223372036854775807)
        System.out.println(i_inf); // 输出 Integer.MAX_VALUE (2147483647)

        float f_inf = (float)d_inf; // d2f
        System.out.println(f_inf); // 输出 Infinity

        float f_nan = (float)d_nan; // d2f
        System.out.println(f_nan); // 输出 NaN
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 5. 对象的创建与访问指令

JVM 对面向对象提供了丰富的字节码支持,包括对象创建、字段访问、数组操作和类型检查。

# 5.1 创建指令

  • new index: 创建一个类实例。
    • 操作数 index 是指向常量池中 CONSTANT_Class_info 的索引,表示要创建的类。
    • 执行过程:
      1. 在堆上分配对象内存(未初始化)。
      2. 将新创建对象的引用压入操作数栈。
    • 注意: new 指令只负责分配内存和压入引用,对象的初始化(调用构造器 <init>)需要后续的 dup 和 invokespecial 指令配合完成。
  • newarray atype: 创建基本类型数组。
    • 操作数 atype 是一个编码,指定数组元素的基本类型 (如 4=boolean, 8=byte, 10=int, 11=long 等)。
    • 执行前,操作数栈顶需要有一个 int 值表示数组长度 (count)。
    • 执行过程:弹出 count,在堆上创建基本类型数组,并将数组引用压入栈。
  • anewarray index: 创建引用类型数组。
    • 操作数 index 是指向常量池中 CONSTANT_Class_info 的索引,表示数组元素的引用类型。
    • 执行前,操作数栈顶需要有一个 int 值表示数组长度 (count)。
    • 执行过程:弹出 count,在堆上创建引用类型数组,并将数组引用压入栈。
  • multianewarray index dimensions: 创建多维数组。
    • index: 指向常量池 CONSTANT_Class_info 的索引,表示数组类型。
    • dimensions: (u1) 表示数组的维度。
    • 执行前,操作数栈需要有 dimensions 个 int 值,表示各维度的长度。
    • 执行过程:弹出所有维度长度,在堆上创建多维数组,并将数组引用压入栈。

代码示例 1: 创建类实例 (new)

// 文件名: NewInstanceTest.java
import java.io.File;

public class NewInstanceTest {
    public void newInstance() {
        // 1. new Object()
        Object obj = new Object();
        // 字节码:
        // new #2           // 创建 Object 实例, 引用入栈 (假设 #2 是 Object Class Info)
        // dup              // 复制栈顶引用 (一个给构造器用, 一个给赋值用)
        // invokespecial #1 // 调用 Object.<init>()V 构造器 (消耗一个引用)
        // astore_1         // 将另一个引用存入 obj (Slot 1)

        // 2. new File("kele")
        File file = new File("kele");
        // 字节码:
        // new #3           // 创建 File 实例, 引用入栈 (假设 #3 是 File Class Info)
        // dup
        // ldc #4           // 加载 "kele" (假设 #4 是 String "kele")
        // invokespecial #5 // 调用 File.<init>(Ljava/lang/String;)V (消耗引用和 String)
        // astore_2         // 将另一个引用存入 file (Slot 2)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

NewInstanceTest 字节码截图 注意 dup 指令在 new 之后、invokespecial 之前的使用,这是标准的对象创建模式。

代码示例 2: 创建数组 (newarray, anewarray, multianewarray)

// 文件名: NewArrayTest.java
public class NewArrayTest {
    public void newArray() {
        // 1. int[]
        int[] intArray = new int[10];
        // 字节码:
        // bipush 10       // 长度 10 入栈
        // newarray 10     // 创建 int 数组 (atype=10), 引用入栈
        // astore_1        // 存入 intArray

        // 2. Object[]
        Object[] objArray = new Object[10];
        // 字节码:
        // bipush 10       // 长度 10 入栈
        // anewarray #6    // 创建 Object 数组 (假设 #6 是 Object Class Info), 引用入栈
        // astore_2        // 存入 objArray

        // 3. int[][] (二维)
        int[][] mintArray = new int[10][10];
        // 字节码:
        // bipush 10       // 第一维长度 10 入栈
        // bipush 10       // 第二维长度 10 入栈
        // multianewarray #7, 2 // 创建 int[10][10] (假设 #7 是 [[I), 维度为 2
        // astore_3        // 存入 mintArray

        // 4. String[][] (只指定第一维)
        String[][] strArray = new String[10][];
        // 字节码:
        // bipush 10       // 长度 10 入栈
        // anewarray #8    // 创建 String[] 数组 (假设 #8 是 String Class Info), 注意这里创建的是 String[]
        // astore 4        // 存入 strArray (实际上存的是 String[] 的引用)
        //  这里创建的是一个包含 10 个 null 的 String[] 数组
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

NewArrayTest 字节码截图

# 5.2 字段访问指令

用于读取或写入对象的实例字段或类的静态字段。

  • 访问静态字段 (类变量):
    • getstatic index: 获取指定类的静态字段的值,并将其压入操作数栈。index 指向常量池 CONSTANT_Fieldref_info。
    • putstatic index: 将操作数栈顶的值赋给指定类的静态字段。index 指向常量池 CONSTANT_Fieldref_info。
  • 访问实例字段:
    • getfield index: 获取指定对象的实例字段的值,并将其压入操作数栈。执行前,栈顶需要有对象引用。执行后,对象引用弹出,字段值入栈。index 指向常量池 CONSTANT_Fieldref_info。
    • putfield index: 将操作数栈顶的值赋给指定对象的实例字段。执行前,栈顶需要依次有 value 和 objectref。执行后,value 和 objectref 都弹出。index 指向常量池 CONSTANT_Fieldref_info。

代码示例 1: getstatic

// 文件名: GetStaticTest.java
public class GetStaticTest {
    public void sayHello() {
      // 调用 System.out.println("Hello");
      System.out.println("Hello");
      // 字节码:
      // getstatic #8 // 获取 System.out 静态字段 (PrintStream 引用) 压栈 (假设 #8 指向 System.out)
      // ldc #9       // 加载 "Hello" 压栈 (假设 #9 是 String "Hello")
      // invokevirtual #10 // 调用 PrintStream.println(String)
      // return
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

图示过程:

  1. getstatic 将 System.out (一个 PrintStream 对象引用) 压栈。 getstatic 后
  2. ldc 将字符串 "Hello" 的引用压栈。 ldc 后
  3. invokevirtual 调用 println 方法。它会消耗栈顶的两个元素:参数 ("Hello") 和调用对象 (System.out)。 invokevirtual 后

代码示例 2: getfield, putfield, putstatic

// 文件名: FieldAccessTest.java
class Order {
    int id; // 实例字段
    static String name; // 静态字段
}

public class FieldAccessTest {
    public void setOrderId() {
        Order order = new Order(); // new, dup, invokespecial, astore_1
        // order.id = 1001; (实例字段赋值)
        // aload_1       // 加载 order 引用
        // sipush 1001   // 加载 1001
        // putfield #field_id // 将 1001 赋给 order.id (假设 #field_id 指向 Order.id)
        order.id = 1001;

        // System.out.println(order.id); (实例字段读取)
        // getstatic #system_out // 加载 System.out
        // aload_1       // 加载 order 引用
        // getfield #field_id // 获取 order.id 的值压栈
        // invokevirtual #println_int // 调用 println(int)
        System.out.println(order.id);

        // Order.name = "ORDER"; (静态字段赋值)
        // ldc #string_order // 加载 "ORDER" (假设 #string_order 指向 "ORDER")
        // putstatic #field_name // 将 "ORDER" 赋给 Order.name (假设 #field_name 指向 Order.name)
        Order.name = "ORDER";

        // System.out.println(Order.name); (静态字段读取)
        // getstatic #system_out // 加载 System.out
        // getstatic #field_name // 获取 Order.name 的值压栈
        // invokevirtual #println_string // 调用 println(String)
        System.out.println(Order.name); // 注意: 实例引用 order 也能访问静态字段, 但字节码层面是 getstatic
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

FieldAccessTest 字节码截图

# 5.3 数组操作指令

  • 加载数组元素到操作数栈 (xaload):
    • baload (byte/boolean), caload (char), saload (short), iaload (int), laload (long), faload (float), daload (double), aaload (reference)。
    • 执行前栈结构: index, arrayref。
    • 执行后: index 和 arrayref 弹出,arrayref[index] 的值压栈。
  • 将操作数栈顶元素存储到数组 (xastore):
    • bastore (byte/boolean), castore (char), sastore (short), iastore (int), lastore (long), fastore (float), dastore (double), aastore (reference)。
    • 执行前栈结构: value, index, arrayref。
    • 执行后: 三个元素弹出,arrayref[index] = value。
  • 获取数组长度 (arraylength):
    • 执行前栈结构: arrayref。
    • 执行后: arrayref 弹出,数组长度 (int) 压栈。

代码示例 1: 数组读写 (iaload, iastore)

// 文件名: ArrayAccessTest.java
public class ArrayAccessTest {
    public void setArray() {
        int[] intArray = new int[10]; // bipush 10, newarray 10, astore_1
        // intArray[3] = 20;
        // aload_1    // 加载 intArray 引用
        // iconst_3   // 加载索引 3
        // bipush 20  // 加载值 20
        // iastore    // intArray[3] = 20
        intArray[3] = 20;

        // System.out.println(intArray[1]);
        // getstatic #... // 加载 System.out
        // aload_1    // 加载 intArray 引用
        // iconst_1   // 加载索引 1
        // iaload     // 加载 intArray[1] 的值压栈
        // invokevirtual #... // 调用 println(int)
        System.out.println(intArray[1]);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

ArrayAccessTest 字节码截图

代码示例 2: 获取数组长度 (arraylength)

// 文件名: ArrayLengthTest.java
public class ArrayLengthTest {
    public void arrLength(){
        double[] arr = new double[10]; // bipush 10, newarray 11 (double), astore_1
        // System.out.println(arr.length);
        // getstatic #... // 加载 System.out
        // aload_1    // 加载 arr 引用
        // arraylength // 获取数组长度压栈 (10)
        // invokevirtual #... // 调用 println(int)
        System.out.println(arr.length);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

ArrayLengthTest 字节码截图

# 5.4 类型检查指令

  • instanceof index: 判断栈顶的对象引用 (objectref) 是否是 index 指向的类或其子类的实例,或者是该接口的实现类实例。
    • index: 指向常量池 CONSTANT_Class_info。
    • 执行过程: 弹出 objectref,将判断结果 (1 for true, 0 for false) 压入栈。
    • 如果 objectref 为 null,结果总是 0。
  • checkcast index: 检查栈顶的对象引用 (objectref) 是否可以强制转换为 index 指向的类型。
    • index: 指向常量池 CONSTANT_Class_info。
    • 执行过程:
      • 如果可以转换(是该类或其子类实例,或是接口实现类实例),则不改变操作数栈(objectref 仍然在栈顶)。
      • 如果 objectref 为 null,可以转换为任何类型,不改变操作数栈。
      • 如果不能转换,抛出 ClassCastException。

代码示例: instanceof 和 checkcast

// 文件名: TypeCheckTest.java
public class TypeCheckTest {
    // 参数 obj 在 Slot 1
    public String checkCast(Object obj) {
        // if (obj instanceof String)
        // aload_1       // 加载 obj
        // instanceof #2 // 检查 obj 是否是 String (假设 #2 是 String Class Info)
        // ifeq 11       // 如果结果为 0 (false), 跳转到 else 分支
        if (obj instanceof String) {
            // return (String) obj;
            // aload_1       // 加载 obj
            // checkcast #2  // 检查 obj 是否能转为 String (如果 instanceof 为 true, 这里总能成功)
            // areturn       // 返回 obj 引用
            return (String) obj;
        } else {
            // return null;
            // aconst_null   // 加载 null
            // areturn       // 返回 null
            return null;
        }
        // 11: (else 分支的开始)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

TypeCheckTest 字节码截图

# 6. 方法调用与返回指令

# 6.1 方法调用指令

JVM 提供了 5 条指令用于调用方法:

  • invokevirtual index: 调用对象的实例方法 (非 private, 非 static, 非 final, 非构造器)。
    • index: 指向常量池 CONSTANT_Methodref_info。
    • 动态分派 (Dynamic Dispatch): 在运行时根据对象的实际类型来确定调用哪个方法。这是实现 Java 多态的关键指令。
    • 执行过程: 从操作数栈弹出 objectref (调用对象) 和所有方法参数,然后执行查找和调用。
    • 最常见的方法调用方式。
  • invokeinterface index count: 调用接口方法。
    • index: 指向常量池 CONSTANT_InterfaceMethodref_info。
    • count: (u1) 早期用于性能优化,现在通常不直接使用,但必须存在。
    • 动态分派: 在运行时搜索实现该接口的对象,找到对应的方法进行调用。
  • invokespecial index: 调用需要特殊处理的实例方法。
    • index: 指向常量池 CONSTANT_Methodref_info。
    • 静态分派 (Static Dispatch): 调用的目标方法在编译期就已确定,不进行动态查找。
    • 主要用于三种情况:
      1. 实例初始化方法 (<init>): 调用构造器。
      2. 私有方法 (private methods)。
      3. 父类方法 (super.method())。
  • invokestatic index: 调用类(静态)方法 (static methods)。
    • index: 指向常量池 CONSTANT_Methodref_info (也可以是 CONSTANT_InterfaceMethodref_info,用于调用接口的静态方法)。
    • 静态分派: 编译期确定目标方法。
  • invokedynamic index: (JDK 7+) 调用动态链接的方法。
    • index: 指向常量池 CONSTANT_InvokeDynamic_info。
    • 动态语言支持核心: 调用目标在运行时通过引导方法 (Bootstrap Method) 来解析。常用于实现 Lambda 表达式、invokedynamic API 等。分派逻辑由用户代码(引导方法)决定。

分派总结:

  • 静态分派: 编译期可知,目标唯一。(invokespecial, invokestatic)
  • 动态分派: 运行时根据对象实际类型决定。(invokevirtual, invokeinterface)
  • 动态语言分派: 运行时由引导方法决定。(invokedynamic)

代码示例 1: invokespecial

// 文件名: InvokeSpecialTest.java
import java.util.Date;

public class InvokeSpecialTest extends Date { // 假设继承自 Date
    // 构造器
    public InvokeSpecialTest() {
        super(); // 隐式调用父类构造器, 使用 invokespecial
    }

    // 实例方法
    public void invoke1(){
        // 1. 调用本类构造器 (如果 new InvokeSpecialTest()) - invokespecial
        // 2. 调用父类方法
        super.toString(); // invokespecial
        // 3. 调用私有方法
        methodPrivate();  // invokespecial
    }

    // 私有方法
    private void methodPrivate(){
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

InvokeSpecialTest 部分字节码截图

代码示例 2: invokestatic

// 文件名: InvokeStaticTest.java
public class InvokeStaticTest {
    public void invoke2(){
        // 调用静态方法
        methodStatic(); // invokestatic
    }
    public static void methodStatic(){
    }
}
1
2
3
4
5
6
7
8
9

InvokeStaticTest 字节码截图

代码示例 3: invokeinterface

// 文件名: InvokeInterfaceTest.java
import java.util.Comparator;

public class InvokeInterfaceTest {
    public void invoke3(){
        // 调用 Runnable 接口的 run 方法
        Thread t1 = new Thread();
        ((Runnable)t1).run(); // invokeinterface

        // 调用 Comparable 接口的 compareTo 方法
        Comparable<Integer> com = null;
        com.compareTo(123);   // invokeinterface
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

InvokeInterfaceTest 字节码截图

代码示例 4: invokevirtual

// 文件名: InvokeVirtualTest.java
public class InvokeVirtualTest {
    public void invoke4(){
        // 调用 PrintStream 的实例方法 println
        System.out.println("hello"); // getstatic System.out, ldc "hello", invokevirtual

        // 调用 Thread 的实例方法 run (即使 t1 为 null, 编译时仍生成 invokevirtual, 运行时 NPE)
        Thread t1 = null;
        t1.run(); // aload_1, invokevirtual
    }
}
1
2
3
4
5
6
7
8
9
10
11

InvokeVirtualTest 字节码截图

代码示例 5: 接口中的 static 和 default 方法

// 文件名: InterfaceMethodTest.java
interface AA{
    // 接口静态方法 (JDK 8+)
    public static void method1(){ }
    // 接口默认方法 (JDK 8+)
    public default void method2(){ }
}
class BB implements AA{ }

public class InterfaceMethodTest {
    public static void main(String[] args) {
        AA aa = new BB();
        // 调用默认方法 method2() -> 通过实例调用
        aa.method2();  // invokeinterface AA.method2:()V

        // 调用接口静态方法 method1() -> 通过接口名调用
        AA.method1(); // invokestatic AA.method1:()V
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

InterfaceMethodTest 字节码截图 默认方法通过 invokeinterface 调用,静态方法通过 invokestatic 调用。

# 6.2 方法返回指令

用于将控制权从当前方法返回给调用者。

  • 根据返回值类型区分:
    • return: 用于 void 方法、实例初始化方法 (<init>)、类初始化方法 (<clinit>)。
    • ireturn: 返回 int, boolean, byte, char, short 类型的值。
    • lreturn: 返回 long。
    • freturn: 返回 float。
    • dreturn: 返回 double。
    • areturn: 返回 reference (对象或数组引用)。
  • 执行过程:
    1. 从当前方法的操作数栈顶弹出返回值 (除了 return)。
    2. 将返回值压入调用者方法的操作数栈。
    3. 如果当前方法是 synchronized 的,隐式执行 monitorexit 释放锁。
    4. 销毁当前方法的栈帧。
    5. 恢复调用者的栈帧,并将 PC 设置为调用指令的下一条指令。

代码示例:

// 文件名: ReturnTest.java
public class ReturnTest {
    public int returnInt() { return 500; }           // sipush 500, ireturn
    public double returnDouble() { return 0.0; }      // dconst_0, dreturn
    public String returnString() { return "hello"; }  // ldc "hello", areturn
    public int[] returnArr() { return null; }         // aconst_null, areturn
    public float returnFloat() { int i=10; return i; }// bipush 10, istore_1, iload_1, i2f, freturn
    public byte returnByte() { return 0; }            // iconst_0, ireturn
    public void methodReturn() { /*...*/ }             // return
}
1
2
3
4
5
6
7
8
9
10

# 7. 操作数栈管理指令

用于直接操纵操作数栈,进行如弹出、复制、交换等操作。这些指令不区分数据类型。

  • 弹出栈顶元素:
    • pop: 弹出栈顶 1 个 Slot 的元素 (如 int, float, reference)。
    • pop2: 弹出栈顶 2 个 Slot 的元素 (如 long, double,或两个单 Slot 元素)。
  • 复制栈顶元素:
    • dup: 复制栈顶 1 个 Slot 的元素,并将复制后的值压入栈顶。栈: ..., value -> ..., value, value。
    • dup2: 复制栈顶 2 个 Slot 的元素 (形式 1: 一个 long/double;形式 2: 两个单 Slot 值),并将复制后的值压入栈顶。
      • 形式 1: ..., value_word1, value_word2 -> ..., value_word1, value_word2, value_word1, value_word2
      • 形式 2: ..., value2, value1 -> ..., value2, value1, value2, value1
    • dup_x1: 复制栈顶 1 个 Slot (value1),插入到栈顶第二个 Slot (value2) 之下。栈: ..., value2, value1 -> ..., value1, value2, value1。
    • dup_x2: 复制栈顶 1 个 Slot (value1),插入到栈顶第三个 Slot (value3/value2) 之下。
      • 形式 1 (value2, value3 是单 Slot): ..., value3, value2, value1 -> ..., value1, value3, value2, value1
      • 形式 2 (value2 是双 Slot): ..., value2_word1, value2_word2, value1 -> ..., value1, value2_word1, value2_word2, value1
    • dup2_x1: 复制栈顶 2 个 Slot (形式 1 或 2),插入到其下的 1 个 Slot (value3) 之下。
      • 形式 1 (复制 long/double): ..., value3, value2_word1, value2_word2 -> ..., value2_word1, value2_word2, value3, value2_word1, value2_word2
      • 形式 2 (复制两个单 Slot): ..., value3, value2, value1 -> ..., value2, value1, value3, value2, value1
    • dup2_x2: 复制栈顶 2 个 Slot (形式 1 或 2),插入到其下的 2 个 Slot 之下。
  • 交换栈顶元素:
    • swap: 交换栈顶 2 个单 Slot 元素的位置。栈: ..., value2, value1 -> ..., value1, value2。 (不支持 long/double)
  • 无操作:
    • nop: (Opcode 0x00) 不执行任何操作。可用于调试、占位或对齐。

代码示例 1: pop (方法返回值未使用)

// 文件名: PopTest1.java
public class PopTest1 {
    public void print() {
        Object obj = new Object(); // new, dup, invokespecial, astore_1
        // 调用 obj.toString() 但不使用其返回值 String
        obj.toString();
        // aload_1 (加载 obj)
        // invokevirtual #... toString() // 返回 String 引用压栈
        // pop // 返回的 String 引用未使用,弹出栈顶 (1 Slot)
    }

    // 如果接收返回值:
    // String info = obj.toString();
    // aload_1
    // invokevirtual #... toString()
    // astore_2 // 将返回的 String 引用存入局部变量 info,不使用 pop
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

PopTest1 字节码截图

代码示例 2: pop2 (long/double 返回值未使用)

// 文件名: PopTest2.java
public class PopTest2 {
    public void foo(){
        // 调用 bar() 但不使用其返回值 long
        bar();
        // invokespecial #... bar() // 返回 long 压栈 (占 2 Slot)
        // pop2 // 返回的 long 未使用,弹出栈顶 (2 Slot)
    }
    public long bar(){
        return 0L; // lconst_0, lreturn
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

PopTest2 字节码截图

代码示例 3: dup, putfield 中的 pop2

// 文件名: DupPopTest.java
public class DupPopTest {
    private long index = 0L; // 实例字段

    public long nextIndex() {
        // return index++; (先返回旧值,index 再自增)
        // 1. 加载 this
        //    aload_0
        // 2. 复制 this 引用 (一个给 getfield, 一个给 putfield)
        //    dup
        // 3. 获取旧的 index 值压栈 (long, 占 2 Slot)
        //    getfield #index
        // 4. 再次复制旧的 index 值 (一个用于返回, 一个用于计算新值) -> 这里是 dup2
        //    dup2
        // 5. 将旧值存起来稍后返回 (需要临时变量或复杂栈操作,但 javap 显示的不一定如此直接)
        // 6. 加载常量 1L
        //    lconst_1
        // 7. 计算新值 (旧值 + 1L)
        //    ladd
        // 8. 将新值存回字段 index (消耗 this 引用和新值)
        //    putfield #index
        // 9. 返回旧值 (之前复制的那个)
        //    lreturn

        // javap 的简化表示可能如下:
        // aload_0         (this)
        // dup             (this, this)
        // getfield #index (this, old_val_word1, old_val_word2)
        // dup2            (this, old_val_w1, old_val_w2, old_val_w1, old_val_w2)
        // lconst_1        (this, old_val_w1, old_val_w2, old_val_w1, old_val_w2, 1L_w1, 1L_w2)
        // ladd            (this, old_val_w1, old_val_w2, new_val_w1, new_val_w2)
        // putfield #index (old_val_w1, old_val_w2)  <-- putfield 消耗 this 和 new_val
        // lreturn         (return old_val)
        return index++;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

DupPopTest 字节码截图 (这个例子的字节码和栈变化比较复杂,dup2 用于同时复制 long 值的两个 Slot。 putfield 会消耗掉对象引用和要赋的值,这里是 this 和计算后的新 index 值)

# 8. 控制转移指令

用于改变程序执行流程,实现分支、循环、跳转等。

# 8.1 比较指令 (lcmp, fcmp*, dcmp*)

  • 已在算术指令部分介绍 (3.6 节)。
  • 它们比较栈顶两个 long, float 或 double 值,并将结果 -1, 0, 或 1 压入栈。
  • 是后续条件跳转指令的基础。

# 8.2 条件跳转指令 (if*)

根据栈顶单个 int 值的条件进行跳转。

  • 指令:
    • ifeq offset: 如果栈顶 int 值 等于 0,跳转。
    • ifne offset: 如果栈顶 int 值 不等于 0,跳转。
    • iflt offset: 如果栈顶 int 值 小于 0,跳转。
    • ifle offset: 如果栈顶 int 值 小于等于 0,跳转。
    • ifgt offset: 如果栈顶 int 值 大于 0,跳转。
    • ifge offset: 如果栈顶 int 值 大于等于 0,跳转。
    • ifnull offset: 如果栈顶引用值为 null,跳转。
    • ifnonnull offset: 如果栈顶引用值不为 null,跳转。
  • 操作数 offset: 2 字节带符号整数,表示相对于当前指令地址的跳转偏移量。
  • 执行过程: 弹出栈顶元素,检查条件。若满足,PC 跳转到 current_pc + offset;否则,继续执行下一条指令。
  • 类型处理:
    • boolean, byte, char, short 的比较结果直接使用这些 if* 指令判断。
    • long, float, double 需要先用 lcmp/fcmp*/dcmp* 指令得到 int 结果 (-1, 0, 1),再用 ifeq, iflt 等指令判断这个结果。

代码示例 1: ifeq

// 文件名: IfEqTest.java
public class IfEqTest {
    public void compare1(){
        int a = 0; // iconst_0, istore_1
        // if(a == 0) { a = 20; } else { a = 10; }
        // iload_1 (加载 a=0)
        // ifeq 12 (如果 a == 0, 跳转到 12) -> 跳转发生
        if(a != 0){ // 源码是 != 0, 但字节码通常优化为 == 0 跳转到 else
            a = 10; // (这部分代码被跳过)
        } else {
            // 12: bipush 20
            // 14: istore_1 (a = 20)
            a = 20;
        }
        // 15: return
    }
}
// 对应字节码片段 (注意与源码逻辑的 slight difference in branching)
//   2: iload_1
//   3: ifeq          12 // 如果 a == 0 跳转到 12 (else 分支)
//   6: bipush        10 // (if 分支, a=10)
//   8: istore_1
//   9: goto          15 // 跳过 else
//  12: bipush        20 // (else 分支, a=20)
//  14: istore_1
//  15: return
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

代码示例 2: ifnonnull

// 文件名: IfNullTest.java
public class IfNullTest {
    // 参数 str 在 Slot 1
    public boolean compareNull(String str){
        // if(str != null) { return false; } else { return true; }
        // aload_1 (加载 str)
        // ifnonnull 6 (如果 str != null, 跳转到 6)
        if(str == null){ // 源码是 == null
            // (如果 nonnull 不跳转, 说明 str == null)
            // iconst_1
            // ireturn (返回 true)
            return true;
        } else {
            // 6: iconst_0
            // 7: ireturn (返回 false)
            return false;
        }
    }
}
// 对应字节码
//   0: aload_1
//   1: ifnonnull     6 // 如果 str != null 跳转到 6 (else 分支)
//   4: iconst_1      // (if 分支, 返回 true)
//   5: ireturn
//   6: iconst_0      // (else 分支, 返回 false)
//   7: ireturn
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

代码示例 3: float 比较 + ifge

// 文件名: FloatCompareTest.java
public class FloatCompareTest {
    public void compare2() {
        float f1 = 9f;  // ldc, fstore_1
        float f2 = 10f; // ldc, fstore_2
        // System.out.println(f1 < f2); // 期望 true
        // getstatic #...
        // fload_1
        // fload_2
        // fcmpg (f1 < f2, 结果 -1 压栈)
        // ifge 19 (如果 -1 >= 0, 跳转) -> 不跳转
        // iconst_1 (压入 1, 代表 true)
        // goto 20
        // 19: iconst_0 (压入 0, 代表 false)
        // 20: invokevirtual #... println(Z)
        System.out.println(f1 < f2);
    }
}
// 对应字节码
//  11: fcmpg  // 比较 f1 和 f2, 结果 -1 入栈
//  12: ifge          19 // 如果 >= 0 跳转 (条件不满足)
//  15: iconst_1
//  16: goto          20
//  19: iconst_0
//  20: invokevirtual #5 // 打印 boolean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

代码示例 4: long 比较 + ifle

// 文件名: LongCompareTest.java
public class LongCompareTest {
    public void compare3() {
        int i1 = 10;   // bipush, istore_1
        long l1 = 20L; // ldc2_w, lstore_2
        // System.out.println(i1 > l1); // 期望 false
        // getstatic #...
        // iload_1
        // i2l (将 i1 转为 long)
        // lload_2
        // lcmp (10L < 20L, 结果 -1 压栈)
        // ifle 21 (如果 -1 <= 0, 跳转) -> 跳转
        // iconst_1 (被跳过)
        // goto 22
        // 21: iconst_0 (压入 0, 代表 false)
        // 22: invokevirtual #... println(Z)
        System.out.println(i1 > l1);
    }
}
// 对应字节码
//  13: lcmp // 比较 long, 结果 -1 入栈
//  14: ifle          21 // 如果 <= 0 跳转 (条件满足)
//  17: iconst_1
//  18: goto          22
//  21: iconst_0
//  22: invokevirtual #5 // 打印 boolean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

代码示例 5: double 比较 + ifle

// 文件名: DoubleCompareTest.java
public class DoubleCompareTest {
    // 参数 d 在 Slot 1, 2
    public int compare4(double d) {
        // if (d > 50.0) { return 1; } else { return -1; }
        // dload_1 (加载 d)
        // ldc2_w #... (加载 50.0)
        // dcmpl (比较 d 和 50.0)
        // ifle 10 (如果 d <= 50.0, 跳转到 else 分支)
        if (d > 50.0) {
            // (如果 dcmpl 结果 > 0, ifle 不跳转)
            // iconst_1
            // ireturn (返回 1)
            return 1;
        } else {
            // 10: iconst_m1 (-1)
            // 11: ireturn (返回 -1)
            return -1;
        }
    }
}
// 对应字节码
//   4: dcmpl // 比较 d 和 50.0
//   5: ifle          10 // 如果 d <= 50.0 跳转到 10 (else 分支)
//   8: iconst_1      // (if 分支)
//   9: ireturn
//  10: iconst_m1     // (else 分支)
//  11: ireturn
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 8.3 比较条件跳转指令 (if_icmp*, if_acmp*)

将比较栈顶两个元素和条件跳转合并为一条指令。

  • 整数比较跳转 (if_icmp*):
    • if_icmpeq offset: 如果栈顶两个 int 相等,跳转。
    • if_icmpne offset: 如果栈顶两个 int 不相等,跳转。
    • if_icmplt offset: 如果次顶 < 栈顶,跳转。
    • if_icmple offset: 如果次顶 <= 栈顶,跳转。
    • if_icmpgt offset: 如果次顶 > 栈顶,跳转。
    • if_icmpge offset: 如果次顶 >= 栈顶,跳转。
  • 引用比较跳转 (if_acmp*):
    • if_acmpeq offset: 如果栈顶两个引用相等 (指向同一对象或都为 null),跳转。
    • if_acmpne offset: 如果栈顶两个引用不相等,跳转。
  • 执行过程: 从操作数栈弹出两个元素进行比较。若满足条件,则跳转;否则继续。比较后,两个元素都被消耗。

代码示例 1: if_icmple

// 文件名: IfIcmpTest1.java
public class IfIcmpTest1 {
    public void ifCompare1(){
        int i = 10; // bipush, istore_1
        int j = 20; // bipush, istore_2
        // System.out.println(i > j); // 期望 false
        // getstatic #...
        // iload_1 (压入 i=10)
        // iload_2 (压入 j=20)
        // if_icmple 18 (比较 i 和 j, 如果 i <= j, 跳转) -> 跳转
        // iconst_1 (被跳过)
        // goto 19
        // 18: iconst_0 (压入 0)
        // 19: invokevirtual #... println(Z)
        System.out.println(i > j);
    }
}
// 对应字节码
//  9: iload_1
// 10: iload_2
// 11: if_icmple     18 // 比较 i, j, 如果 i <= j 跳转 (满足)
// 14: iconst_1
// 15: goto          19
// 18: iconst_0
// 19: invokevirtual #5 // 打印 boolean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

代码示例 2: short/byte 比较 (隐式转 int)

// 文件名: IfIcmpTest2.java
public class IfIcmpTest2 {
    public void ifCompare2() {
        short s1 = 9; // bipush, istore_1
        byte b1 = 10; // bipush, istore_2
        // System.out.println(s1 > b1); // 期望 false
        // ...
        // iload_1 (加载 s1, 已是 int)
        // iload_2 (加载 b1, 已是 int)
        // if_icmple 18 (比较 9 <= 10, 跳转) -> 跳转
        // ... (同上)
        System.out.println(s1 > b1);
    }
}
// 字节码与示例 1 非常相似
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

代码示例 3: if_acmpne, if_acmpeq

// 文件名: IfAcmpTest.java
public class IfAcmpTest {
    public void ifCompare3() {
        Object obj1 = new Object(); // new... astore_1
        Object obj2 = new Object(); // new... astore_2

        // System.out.println(obj1 == obj2); // 期望 false
        // getstatic #...
        // aload_1
        // aload_2
        // if_acmpne 28 (比较 obj1, obj2 引用, 不相等则跳转) -> 跳转
        // iconst_1 (被跳过)
        // goto 29
        // 28: iconst_0
        // 29: invokevirtual #... println(Z)
        System.out.println(obj1 == obj2);

        // System.out.println(obj1 != obj2); // 期望 true
        // getstatic #...
        // aload_1
        // aload_2
        // if_acmpeq 44 (比较 obj1, obj2 引用, 相等则跳转) -> 不跳转
        // iconst_1
        // goto 45
        // 44: iconst_0 (被跳过)
        // 45: invokevirtual #... println(Z)
        System.out.println(obj1 != obj2);
    }
}
// 对应字节码
//  19: aload_1
//  20: aload_2
//  21: if_acmpne     28 // 不相等则跳转 (满足)
//  ...
//  35: aload_1
//  36: aload_2
//  37: if_acmpeq     44 // 相等则跳转 (不满足)
//  ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 8.4 多条件分支跳转 (tableswitch, lookupswitch)

用于实现 switch-case 语句。

  • tableswitch: 用于 case 值是连续或近似连续的情况。
    • 结构: tableswitch <default_offset> <low_value> <high_value> <jump_offset_for_low> ... <jump_offset_for_high>
    • 执行: 从操作数栈弹出 index (int)。
      • 如果 index 在 [low, high] 范围内,则跳转到 base_pc + jump_offset_for_index。
      • 否则,跳转到 base_pc + default_offset。
    • 效率高: 通过索引直接计算偏移量,O(1) 时间复杂度。 tableswitch 结构
  • lookupswitch: 用于 case 值是离散或稀疏的情况。
    • 结构: lookupswitch <default_offset> <n_pairs> <match1>:<offset1> <match2>:<offset2> ... <matchN>:<offsetN> (pairs 按 match 值排序)
    • 执行: 从操作数栈弹出 key (int)。
      • 在 match-offset 对中搜索等于 key 的 match。
      • 如果找到,跳转到 base_pc + offset。
      • 如果找不到,跳转到 base_pc + default_offset。
    • 效率较低: 需要进行搜索,时间复杂度 O(logN) 或 O(N),取决于实现。 lookupswitch 结构

编译器选择: 编译器会根据 case 值的分布情况自动选择使用 tableswitch (如果 case 值密集) 或 lookupswitch (如果 case 值稀疏)。

代码示例 1: tableswitch (case 1, 2, 3)

// 文件名: TableSwitchTest.java
public class TableSwitchTest {
    // 参数 select 在 Slot 1
    public void swtich1(int select){
        int num; // Slot 2
        switch(select){
            case 1: num = 10; break; // 跳转 28
            case 2: num = 20;        // 跳转 34 (无 break, 会贯穿)
            case 3: num = 30; break; // 跳转 37 (case 2 会执行到这里)
            default: num = 40;       // 跳转 43
        }
        // break 对应 goto 指令跳出 switch
    }
}
// 对应字节码
//   0: iload_1
//   1: tableswitch   { // 1 to 3
//                  1: 28; // case 1 跳转到 PC=28
//                  2: 34; // case 2 跳转到 PC=34
//                  3: 37; // case 3 跳转到 PC=37
//            default: 43  // default 跳转到 PC=43
//         }
//  28: bipush        10
//  30: istore_2      // num = 10
//  31: goto          46 // break (跳到 return)
//  34: bipush        20
//  36: istore_2      // num = 20 (无 break)
//  37: bipush        30
//  39: istore_2      // num = 30
//  40: goto          46 // break
//  43: bipush        40
//  45: istore_2      // num = 40
//  46: return
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

代码示例 2: lookupswitch (case 100, 500, 200)

// 文件名: LookupSwitchTest.java
public class LookupSwitchTest {
    // 参数 select 在 Slot 1
    public void swtich2(int select){
        int num; // Slot 2
        switch(select){
            case 100: num = 10; break; // 跳转 36
            case 500: num = 20; break; // 跳转 42
            case 200: num = 30; break; // 跳转 48
            default: num = 40;        // 跳转 54
        }
    }
}
// 对应字节码 (注意 case 顺序可能被重排)
//   0: iload_1
//   1: lookupswitch  { // 3 pairs
//                100: 36; // key 100 跳转到 PC=36
//                200: 48; // key 200 跳转到 PC=48
//                500: 42; // key 500 跳转到 PC=42
//            default: 54  // default 跳转到 PC=54
//         }
//  36: bipush        10
//  38: istore_2      // num = 10
//  39: goto          57 // break
//  42: bipush        20
//  44: istore_2      // num = 20
//  45: goto          57 // break
//  48: bipush        30
//  50: istore_2      // num = 30
//  51: goto          57 // break
//  54: bipush        40
//  56: istore_2      // num = 40
//  57: return
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

代码示例 3: String switch (JDK 7+)

// 文件名: StringSwitchTest.java
public class StringSwitchTest {
    // 参数 season 在 Slot 1
    public void swtich3(String season){
        // Java 7 开始支持 String switch
        // 编译器实现通常分两步:
        // 1. 使用 String.hashCode() 配合 lookupswitch 跳转到对应 case 的代码块。
        // 2. 在每个 case 代码块开头,使用 String.equals() 再次确认,防止哈希碰撞。
        // 3. 确认后,执行 case 逻辑或跳转到下一个 switch (tableswitch) 处理 break/fallthrough。
        switch(season){
            case "SPRING":break;
            case "SUMMER":break;
            case "AUTUMN":break;
            case "WINTER":break;
        }
    }
}
// 对应字节码 (非常复杂)
//   0: aload_1
//   1: astore_2      // 复制 season 引用
//   2: iconst_m1     // 默认 case 索引 -1
//   3: istore_3
//   4: aload_2
//   5: invokevirtual #hashcode // 计算 season.hashCode()
//   8: lookupswitch  { // 根据 hashCode 跳转
//      -1842350579:  52; // hashCode("SPRING")
//      -1837878353:  66; // hashCode("SUMMER")
//      -1734407483:  94; // hashCode("WINTER")
//       1941980694:  80; // hashCode("AUTUMN")
//          default: 105
//         }
//  // --- Case "SPRING" ---
//  52: aload_2
//  53: ldc           #spring_const // 加载 "SPRING" 常量
//  55: invokevirtual #equals // 调用 season.equals("SPRING")
//  58: ifeq          105 // 如果不相等, 跳到 switch 结束
//  61: iconst_0      // 如果相等, 设置 case 索引为 0
//  62: istore_3
//  63: goto          105 // 跳到 switch 结束
//  // --- Case "SUMMER" --- (类似)
//  66: aload_2
//  67: ldc           #summer_const
//  ...
//  75: iconst_1
//  76: istore_3
//  77: goto          105
//  // --- Case "AUTUMN" --- (类似)
//  80: aload_2
//  81: ldc           #autumn_const
//  ...
//  89: iconst_2
//  90: istore_3
//  91: goto          105
//  // --- Case "WINTER" --- (类似)
//  94: aload_2
//  95: ldc           #winter_const
//  ...
// 103: iconst_3
// 104: istore_3
// // --- Switch 结束, 根据 case 索引处理 break ---
// 105: iload_3       // 加载最终确定的 case 索引
// 106: tableswitch   { // 0 to 3
//                   0: 136; // case 0 ("SPRING") 跳转
//                   1: 139; // case 1 ("SUMMER") 跳转
//                   2: 142; // case 2 ("AUTUMN") 跳转
//                   3: 145; // case 3 ("WINTER") 跳转
//             default: 145  // default (-1) 或 fallthrough 跳转
//          }
// 136: goto          145 // "SPRING" 的 break
// 139: goto          145 // "SUMMER" 的 break
// 142: goto          145 // "AUTUMN" 的 break
// 145: return        // "WINTER" 的 break 或 default
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

String switch 的字节码实现比基本类型 switch 复杂得多。

# 8.5 无条件跳转 (goto, goto_w)

强制将 PC 跳转到指定位置。

  • goto offset: 跳转到 current_pc + offset。offset 是 2 字节带符号整数。
  • goto_w offset: 功能同 goto,但 offset 是 4 字节带符号整数,支持更大的跳转范围。
  • jsr, ret: 已废弃,用于早期 finally 实现,不推荐使用。

代码示例 1: while 循环 (int)

// 文件名: WhileIntTest.java
public class WhileIntTest {
    public void whileInt() {
        int i = 0; // iconst_0, istore_1
        // 循环条件检查 (标签 L_LOOP_START):
        // 2: iload_1
        // 3: bipush 100
        // 5: if_icmpge 17 (如果 i >= 100, 跳转到循环结束 L_LOOP_END)
        while (i < 100) {
            // 循环体:
            String s = "youngkbt.cn"; // ldc, astore_2
            // i++;
            i++; // iinc 1 by 1
            // 无条件跳转回循环条件检查
            // 14: goto 2 (跳转回 L_LOOP_START)
        }
        // 循环结束 (标签 L_LOOP_END):
        // 17: return
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

代码示例 2: while 循环 (double)

// 文件名: WhileDoubleTest.java
public class WhileDoubleTest {
    public void whileDouble() {
        double d = 0.0; // dconst_0, dstore_1
        // L_LOOP_START:
        // 2: dload_1
        // 3: ldc2_w #... (100.1)
        // 6: dcmpg (比较 d 和 100.1)
        // 7: ifge 20 (如果 d >= 100.1, 跳转到 L_LOOP_END)
        while(d < 100.1) {
            String s = "youngkbt.cn"; // ldc, astore_3
            // d++;
            // dload_1
            // dconst_1
            // dadd
            // dstore_1
            d++;
            // 17: goto 2 (跳转回 L_LOOP_START)
        }
        // L_LOOP_END:
        // 20: return
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

代码示例 3: for 循环

// 文件名: ForTest.java
public class ForTest {
    public void printFor() {
        // short i; (局部变量声明)
        // 初始化: i = 0
        // 0: iconst_0
        // 1: istore_1 (i 在 Slot 1)
        // 循环条件检查 (L_LOOP_COND):
        // 2: iload_1
        // 3: bipush 100
        // 5: if_icmpge 19 (如果 i >= 100, 跳转到 L_LOOP_END)
        for (short i = 0; i < 100; i++) { // 注意 i 的类型
            // 循环体:
            String s = "youngkbt.cn"; // ldc, astore_2
            // 更新语句: i++ (注意 short -> int -> short)
            // iload_1 (加载 i)
            // iconst_1
            // iadd    (i+1, 结果是 int)
            // i2s     (将 int 转回 short)
            // istore_1 (存回 i)
        }
        // 无条件跳转回循环条件检查
        // 16: goto 2 (跳转回 L_LOOP_COND)
        // 循环结束 (L_LOOP_END):
        // 19: return
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

代码练习: while vs for vs do-while 字节码对比

// 文件名: LoopCompare.java
public class LoopCompare {
    // while: 条件检查在循环体之前
    public void whileTest(){
        int i = 1;     // iconst_1, istore_1
        // L_COND:
        // iload_1
        // bipush 100
        // if_icmpgt 14 // 如果 i > 100 跳转 L_END
        while(i <= 100){
            // L_BODY:
            i++;         // iinc 1 by 1
            // goto L_COND
        }
        // L_END: return
    }

    // for: 结构类似 while, 初始化只执行一次
    public void forTest(){
        // 初始化: int i = 1; (只执行一次)
        // iconst_1, istore_1
        // goto L_COND // 先跳到条件检查
        // L_BODY: (循环体为空)
        // L_UPDATE: i++;
        // iinc 1 by 1
        // L_COND: 条件检查
        // iload_1
        // bipush 100
        // if_icmple 8 // 如果 i <= 100 跳转回 L_BODY (实际跳转到更新 L_UPDATE)
        for(int i = 1; i <= 100; i++){
        }
        // L_END: return
        // 注意: for 循环变量 i 的作用域仅限循环内
    }

    // do-while: 循环体先执行,条件检查在后
    public void doWhileTest(){
        int i = 1;     // iconst_1, istore_1
        // L_BODY:
        do{
            i++;         // iinc 1 by 1
            // L_COND: 条件检查
            // iload_1
            // bipush 100
            // if_icmple 2 // 如果 i <= 100 跳转回 L_BODY
        } while(i <= 100);
        // L_END: return
    }
}
// 字节码大致结构:
// whileTest: L_COND -> [Body -> goto L_COND] or L_END
// forTest:   Init -> goto L_COND -> [Body -> Update -> L_COND] or L_END
// doWhileTest: L_BODY -> L_COND -> [goto L_BODY] or L_END
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# 9. 异常处理指令

# 9.1 抛出异常指令 (athrow)

  • 作用: 用于显式抛出异常(对应 Java 代码中的 throw 语句)。
  • 执行过程:
    1. 要求操作数栈顶必须是一个异常对象的引用 (Throwable 或其子类)。
    2. 弹出该异常对象引用。
    3. 清空当前方法的操作数栈。
    4. 查找当前方法的异常表 (Exception Table),看是否有能处理该异常类型的 catch 块。
    5. 如果找到匹配的处理器,将 PC 设置到处理器的起始地址,并将异常对象引用压入新的(空的)操作数栈。
    6. 如果未找到匹配的处理器,则弹出当前方法的栈帧,并将异常对象重新抛给调用者方法,重复步骤 4-6。
    7. 如果一直到虚拟机栈底都没有找到处理器,则线程终止。
  • 隐式抛出: JVM 在执行某些指令检测到错误时也会自动抛出异常(如 idiv 除零抛 ArithmeticException, checkcast 类型不匹配抛 ClassCastException),其后续处理流程与 athrow 类似。

代码示例 1: 显式 throw

// 文件名: ThrowTest1.java
public class ThrowTest1 {
    // 参数 i 在 Slot 1
    public void throwZero(int i){
        // if (i != 0) return;
        // iload_1
        // ifne 14 // 如果 i != 0, 跳转到 return
        if(i == 0){
            // (如果 i == 0, ifne 不跳转)
            // new #... RuntimeException // 创建异常对象, 引用压栈
            // dup // 复制引用 (一个给构造器, 一个给 athrow)
            // ldc #... "参数值为0" // 加载错误消息
            // invokespecial #... <init>(String) // 调用构造器
            // athrow // 弹出异常对象引用, 开始异常处理流程
            throw new RuntimeException("参数值为0");
        }
        // 14: return
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

代码示例 2: throws 声明与 throw

// 文件名: ThrowTest2.java
import java.io.IOException;

public class ThrowTest2 {
    // throws 声明只影响编译期检查和方法签名, 不直接生成字节码
    // 参数 i 在 Slot 1
    public void throwOne(int i) throws RuntimeException, IOException {
        // if (i != 1) return;
        // iload_1
        // iconst_1
        // if_icmpne 15 // 如果 i != 1, 跳转到 return
        if(i == 1){
            // (如果 i == 1, 不跳转)
            // new #... RuntimeException
            // dup
            // ldc #... "参数值为1"
            // invokespecial #... <init>(String)
            // athrow
            throw new RuntimeException("参数值为1");
        }
        // 15: return
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 9.2 异常处理与异常表 (Exception Table)

  • 异常处理机制: Java 的 try-catch-finally 结构不是通过特定的字节码指令实现的,而是依赖于每个方法关联的异常表。
  • 异常表:
    • 存储在 Class 文件方法表的 Code 属性中。
    • 是一个包含多个条目 (exception_info) 的表。
    • 每个条目定义了一个异常处理器,包含:
      • start_pc: 处理器监控的代码块起始字节码偏移量 (包含)。
      • end_pc: 处理器监控的代码块结束字节码偏移量 (不包含)。
      • handler_pc: 异常处理代码 (catch 块) 的起始字节码偏移量。
      • catch_type: 指向常量池 CONSTANT_Class_info 的索引,表示该处理器捕获的异常类型。如果为 0,表示捕获任何异常(通常用于 finally)。
  • 工作流程: 当在 [start_pc, end_pc) 范围内抛出异常时,JVM 遍历异常表:
    1. 查找 catch_type 与抛出的异常类型匹配(或为其父类)的条目。
    2. 如果找到,将 PC 设置为对应条目的 handler_pc,并将异常对象压栈,开始执行 catch 块。
    3. 如果未找到,或者在 catch 块中又抛出新异常,则重复在调用者方法中查找异常表的过程。
  • finally 的实现: 编译器会复制 finally 块的代码:
    1. 在 try 块正常结束(如 return 或执行完最后一句)之前插入一份 finally 代码。
    2. 在每个 catch 块结束之前插入一份 finally 代码。
    3. 异常表中会有一个 catch_type 为 0 的条目,其 handler_pc 指向一份 finally 代码,用于处理 try 或 catch 块中抛出但未被捕获的异常。该 finally 代码执行完后会重新抛出异常 (athrow)。

代码示例 1: try-catch

// 文件名: TryCatchTest.java
import java.io.*;

public class TryCatchTest {
    public void tryCatch(){
        try{ // try 块对应字节码 PC: 0 - 22
            File file = new File("d:/hello.txt"); // 0-9
            FileInputStream fis = new FileInputStream(file); // 10-18
            String info = "hello!"; // 19-21
        } catch (FileNotFoundException e) { // catch 块 1 Handler PC: 25
            e.printStackTrace(); // 26-27
        } catch(RuntimeException e){ // catch 块 2 Handler PC: 33
            e.printStackTrace(); // 34-35
        }
        // 正常结束路径
        // 22: goto 38 (跳过 catch 块)
        // 异常处理跳转点
        // 25: astore_1 (异常对象存入 Slot 1) ... 处理 FileNotFoundException
        // 30: goto 38 (处理完后跳到 return)
        // 33: astore_1 (异常对象存入 Slot 1) ... 处理 RuntimeException
        // ...
        // 38: return
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

对应异常表 (javap -v):

Exception table:
   from    to  target type
       0    22      25   Class java/io/FileNotFoundException // try 块 (0-21) 发生 FNFException, 跳转到 25
       0    22      33   Class java/lang/RuntimeException   // try 块 (0-21) 发生 RuntimeException, 跳转到 33
1
2
3
4

TryCatchTest 异常表示例

代码示例 2: try-finally 返回值问题 (面试常考)

// 文件名: FinallyReturnTest.java
public class FinallyReturnTest {
    public static String func() {
        String str = "hello"; // Slot 0
        try {
            // 1. 将要返回的值 "hello" 保存到一个临时局部变量 (Slot 1)
            //    aload_0, astore_1
            // 2. 执行 finally 块代码
            //    ldc "kele", astore_0 (修改了 str, 但不影响临时变量)
            // 3. 从临时局部变量加载返回值 "hello"
            //    aload_1
            // 4. 返回 "hello"
            //    areturn
            return str;
        } finally {
            str = "kele"; // finally 块会修改 str, 但 try 的返回值已暂存
        }
    }

    public static void main(String[] args) {
        System.out.println(func()); // 输出 "hello"
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

对应字节码 (简化逻辑):

   0: ldc           #17 // "hello"
   2: astore_0      // str = "hello"
   // --- try block ---
   3: aload_0       // 加载 str ("hello")
   4: astore_1      // 将返回值暂存到 Slot 1
   // --- finally block (normal path) ---
   5: ldc           #18 // "kele"
   7: astore_0      // str = "kele" (修改 Slot 0)
   // --- return from try (using saved value) ---
   8: aload_1       // 加载暂存的返回值 ("hello" from Slot 1)
   9: areturn       // 返回 "hello"

   // --- Exception Handler for finally ---
  10: astore_2      // 保存异常对象
   // --- finally block (exception path) ---
  11: ldc           #18 // "kele"
  13: astore_0      // str = "kele"
   // --- rethrow exception ---
  14: aload_2       // 加载异常对象
  15: athrow        // 重新抛出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

对应异常表:

Exception table:
   from    to  target type
       3     5      10   any // try 块 (3-4) 发生任何异常, 跳转到 10 执行 finally 并 rethrow
1
2
3

FinallyReturnTest 异常表示例 关键在于 try 块的 return 会将返回值暂存,finally 对原变量的修改不影响这个暂存值。

# 10. 同步控制指令 (monitorenter, monitorexit)

用于支持 Java 的 synchronized 关键字,实现线程同步。

# 10.1 同步机制概述

JVM 支持两种同步方式:

  1. 方法级同步: 使用 synchronized 修饰方法。
  2. 代码块同步: 使用 synchronized (object) { ... } 语句块。

两种方式都依赖于对象监视器 (Monitor) 来实现。每个 Java 对象都有一个关联的监视器。

# 10.2 方法级同步的实现

  • 隐式实现: 方法级的同步不使用 monitorenter/monitorexit 字节码指令。
  • 访问标志: JVM 通过检查方法表结构中的 ACC_SYNCHRONIZED 访问标志来识别同步方法。
  • 执行流程:
    1. 调用同步方法时,JVM 自动获取该方法所属对象(实例方法)或类对象(静态方法)的监视器锁。
    2. 执行方法体。
    3. 方法正常完成或异常终止时,JVM 自动释放锁。

代码示例:

// 文件名: SyncMethodTest.java
public class SyncMethodTest {
    private int i = 0;
    // 同步实例方法
    public synchronized void add() { // 方法表 flags 包含 ACC_SYNCHRONIZED
      i++;
    }
}
1
2
3
4
5
6
7
8

add 方法的字节码 (与非同步版本相同):

   0: aload_0
   1: dup
   2: getfield      #2 // i
   5: iconst_1
   6: iadd
   7: putfield      #2 // i
  10: return
1
2
3
4
5
6
7

字节码本身没有同步指令,同步由 JVM 根据 ACC_SYNCHRONIZED 标志在方法调用和返回时处理。

# 10.3 代码块同步的实现

  • 显式指令: 使用 monitorenter 和 monitorexit 指令实现。
  • monitorenter:
    1. 需要操作数栈顶有一个对象引用 (objectref),作为要锁定的对象。
    2. 尝试获取 objectref 关联监视器的锁。
    3. 如果获取成功(锁计数器为 0,或当前线程已持有该锁),则将锁计数器加 1,弹出 objectref。
    4. 如果获取失败,线程阻塞等待。
  • monitorexit:
    1. 需要操作数栈顶有一个对象引用 (objectref),必须是之前 monitorenter 锁定的同一个对象。
    2. 将 objectref 关联监视器的锁计数器减 1。
    3. 如果计数器变为 0,则释放锁,唤醒等待该锁的线程。
    4. 弹出 objectref。
  • 编译器保证: 编译器必须确保每个 monitorenter 都有至少一个对应的 monitorexit 在所有可能的执行路径上(正常或异常)被执行。
    • 正常路径: 在同步代码块末尾插入 monitorexit。
    • 异常路径: 编译器自动生成一个隐藏的异常处理器 (catch-any),其处理逻辑是执行 monitorexit 释放锁,然后重新抛出异常 (athrow)。

代码示例:

// 文件名: SyncBlockTest.java
public class SyncBlockTest {
    private Object obj = new Object(); // Slot 1: obj
    private int i = 0;             // Slot 2: i

    public void subtract(){
        // synchronized (obj) { ... }
        // 1. 获取锁对象 obj 引用压栈
        //    aload_0
        //    getfield #obj
        // 2. 复制一份引用 (给 monitorenter 和 monitorexit 用)
        //    dup
        // 3. 将复制的引用存入临时局部变量 (例如 Slot 1)
        //    astore_1
        // 4. 进入同步块,获取锁
        //    monitorenter (消耗栈顶的 obj 引用)
        synchronized (obj){
            // --- 同步代码块 ---
            // aload_0
            // dup
            // getfield #i
            // iconst_1
            // isub
            // putfield #i
            i--;
            // --- 正常退出同步块,释放锁 ---
            // aload_1 (加载之前保存的 obj 引用)
            // monitorexit
        } // 编译器在此处插入 monitorexit (正常路径)
        // goto L_AFTER_SYNC // 跳过异常处理代码
        // --- 异常处理程序 (由编译器生成) ---
        // L_EXCEPTION_HANDLER:
        // astore_2 // 保存异常对象
        // aload_1 (加载之前保存的 obj 引用)
        // monitorexit // 异常路径释放锁
        // aload_2 (加载异常对象)
        // athrow // 重新抛出异常
        // L_AFTER_SYNC:
        // return
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

对应字节码:

   0: aload_0
   1: getfield      #4 // obj
   4: dup
   5: astore_1      // 将 obj 存入 Slot 1 (用于后续 monitorexit)
   6: monitorenter  // 获取 obj 的锁
   // --- try block (同步代码块) ---
   7: aload_0
   8: dup
   9: getfield      #2 // i
  12: iconst_1
  13: isub
  14: putfield      #2 // i
  17: aload_1       // 加载 obj (来自 Slot 1)
  18: monitorexit   // 正常退出,释放锁
  19: goto          27 // 跳转到 return
  // --- exception handler (catch-any) ---
  22: astore_2      // 保存异常对象
  23: aload_1       // 加载 obj (来自 Slot 1)
  24: monitorexit   // 异常退出,释放锁
  25: aload_2       // 加载异常对象
  26: athrow        // 重新抛出
  // --- end ---
  27: return
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

对应异常表:

Exception table:
   from    to  target type
       7    19      22   any // 同步块 (7-18) 发生任何异常, 跳转到 22
      22    25      22   any // 异常处理块自身也可能抛异常 (虽然少见), 再次跳转到 22 (防止锁泄漏)
1
2
3
4

SyncBlockTest 异常表示例 编译器通过异常表和冗余的 monitorexit 确保了锁在各种情况下都能被正确释放。

编辑此页 (opens new window)
上次更新: 2025/04/05, 20:16:54
JVM - Class文件结构
JVM - 类的加载过程详解

← JVM - Class文件结构 JVM - 类的加载过程详解→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式