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

黑马JVM解析笔记(四):Javap图解指令流程,深入理解Java字节码执行机制

引言

在Java开发中,理解字节码指令的执行流程对于掌握Java虚拟机(JVM)的工作原理至关重要。本文将通过一个具体案例,结合javap反编译结果,详细图解Java字节码指令的执行过程,帮助读者深入理解JVM的运行时机制。

1. 案例Java代码

public class Demo3_1 {public static void main(String[] args) {int a = 10;int b = Short.MAX_VALUE + 1;  // 32767 + 1 = 32768int c = a + b;System.out.println(c);  // 输出32778}
}

2. Class文件反编译结果

使用javap -v命令反编译class文件,我们主要关注main方法的字节码:

public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=4, args_size=10: bipush        10   // 将10压入操作数栈2: istore_1           // 存储到局部变量1(a)3: ldc           #3   // 加载常量池#3项(32768)5: istore_2           // 存储到局部变量2(b)6: iload_1            // 加载局部变量1(a)7: iload_2            // 加载局部变量2(b)8: iadd               // 执行加法9: istore_3           // 结果存储到局部变量3(c)10: getstatic     #4   // 获取System.out13: iload_3            // 加载局部变量3(c)14: invokevirtual #5   // 调用println方法17: return             // 方法返回

关键信息解析

  • descriptor([Ljava/lang/String;)V 表示方法参数为String数组,返回类型为void
  • flagsACC_PUBLIC, ACC_STATIC 表示public static方法
  • stack=2, locals=4, args_size=1:操作数栈深度为2,局部变量表大小为4(含参数args),1个参数
  • 常量池#3 对应32768,#4 对应System.out字段,#5 对应println方法

编译期优化Short.MAX_VALUE + 1在编译时被计算为常量32768,直接存储在常量池中

3. 字节码执行流程详解

3.1 JVM运行时结构初始化

JVM运行时结构初始化

  • 方法区:加载类信息、常量池和字节码指令
  • :为main方法创建栈帧,包含局部变量表和操作数栈
  • 程序计数器:指向当前执行的指令地址

3.2 指令执行流程图解

1. bipush 10:将10压入操作数栈

bipush 10

  • 将整数值10推送到操作数栈顶
  • 类似指令:sipush(short值),ldc(int值)
2. istore_1:存储到局部变量1(a)

istore_1

  • 弹出栈顶值(10)并存入局部变量表slot 1
  • 局部变量表:slot 0=args, slot 1=10
3. ldc #3:加载常量池#3项(32768)

ldc #3

  • 从常量池加载#3项(32768)到操作数栈
  • 常量池在类加载时已初始化
4. istore_2:存储到局部变量2(b)

istore_2

  • 弹出栈顶值(32768)存入局部变量表slot 2
  • 局部变量表:slot 0=args, slot 1=10, slot 2=32768
5. iload_1iload_2:加载局部变量到操作数栈

iload指令

  • iload_1:加载slot 1的值(10)到操作数栈
  • iload_2:加载slot 2的值(32768)到操作数栈
  • 操作数栈状态:[10, 32768]
6. iadd:执行加法操作

iadd指令

  • 弹出栈顶两个元素(10和32768)
  • 执行加法运算:10 + 32768 = 32778
  • 结果压回操作数栈:[32778]
7. istore_3:结果存储到局部变量3©

istore_3

  • 弹出栈顶值(32778)存入局部变量表slot 3
  • 局部变量表完整状态:args, a=10, b=32768, c=32778
8. getstatic #4:获取System.out静态字段

getstatic #4

  • 解析常量池#4项(System.out字段引用)
  • 获取PrintStream对象引用压入操作数栈
9. iload_3:加载局部变量3©到操作数栈

iload_3

  • 加载slot 3的值(32778)到操作数栈
  • 操作数栈状态:[PrintStream@ref, 32778]
10. invokevirtual #5:调用println方法

invokevirtual #5

  1. 解析常量池#5项(println方法引用)
  2. 创建新的栈帧用于println方法
  3. 传递参数(32778)到新栈帧
  4. 执行println方法打印结果
11. return:方法返回
  • 弹出main方法栈帧
  • 程序执行结束

4. 关键指令深度解析

getstatic #4 指令详解

getstatic指令用于获取类的静态字段值,其执行过程如下:

  1. 解析字段引用

    • 根据常量池索引#4找到Fieldref常量
    • 解析出类名(java/lang/System)、字段名(out)和类型(Ljava/io/PrintStream;)
  2. 类加载检查

    • 如果System类未加载,触发类加载过程
    • 验证字段访问权限(public静态字段可直接访问)
  3. 获取字段值

    // 伪代码表示字段访问过程
    Class systemClass = ClassLoader.loadClass("java.lang.System");
    Field outField = systemClass.getField("out");
    Object printStream = outField.get(null); // 静态字段,实例参数为null
    
  4. 值压栈

    • 将获取的PrintStream引用压入操作数栈

性能提示:首次执行getstatic会有解析开销,后续执行直接使用缓存结果

方法调用过程

invokevirtual指令执行流程:

  1. 创建新的栈帧(包含局部变量表和操作数栈)
  2. 将调用者对象(System.out)和参数(c的值)压入新栈帧
  3. 转移控制权到新方法
  4. 方法执行完毕后,返回值压入调用者栈帧的操作数栈
  5. 销毁被调用方法的栈帧

5. 总结与思考

通过本案例的图解分析,我们可以得出以下结论:

  1. 编译期优化:常量表达式在编译阶段计算并存入常量池
  2. 栈式架构:JVM使用操作数栈作为计算中间结果的存储区
  3. 局部变量表:方法参数和局部变量存储在固定大小的表中
  4. 指令原子性:每个字节码指令完成一个基本操作
  5. 方法调用:涉及新栈帧的创建和上下文切换

相关文章:

  • 【办公类-105-01】20250626 托小班报名表-条件格式-判断双胞胎EXCLE
  • MySQL (一):数据类型,完整性约束和表间关系
  • C++智能指针概念及std::unique_ptr使用介绍
  • YOLOv10tensorRT推理代码C++
  • Apipost和Postman对比
  • Git-git worktree的使用
  • [OS_27] 现代应用程序架构 | Full System Emulation | VM | Docker
  • springcloud 尚硅谷 看到9开头
  • C++(模板与容器)
  • 【学习笔记】Dify知识库配置参数解析
  • 跨越十年的C++演进:C++14新特性全解析
  • CNN不是一个模型?
  • [特殊字符] Windows 查看端口占用及服务来源教程(以 9018 端口为例)
  • Python网安-随机密码生成器
  • 架构轻巧的kokoro 文本转语音模型
  • tcpdump用法
  • 快速傅里叶变换(FFT)是什么?
  • 以太网基础与 VLAN 配置实验
  • HTML 按钮单击事件示例
  • OceanBase批量插入数据报错java.lang.ArrayIndexOutOfBoundsException:0