为什么 Java 一行代码,JVM 要执行 4 条指令?(99% Java 开发没真正看过)

为什么 Java 一行代码,JVM 要执行 4 条指令?(99% Java 开发没真正看过)

JVM 字节码实战:深入解析 System.out.println 的执行原理

标签:Java | JVM | 字节码 | javap | 类文件结构 | 运行时数据区
难度:进阶
阅读时间:10 分钟


一、前言:为什么需要了解字节码?

作为 Java 开发者,我们每天都在写这样的代码:

System.out.println("hello world");

但很少有人思考:JVM 到底如何执行这行代码?

本文将通过 javap 工具,结合 JVM 内存结构,带你从字节码层面理解 JVM 的执行模型。


二、JVM 运行时数据区全景

在分析字节码之前,先建立 JVM 内存结构的整体认知:

在这里插入图片描述

图 1:JVM 运行时数据区与执行流程全景图

2.1 关键区域说明

区域线程私有作用本文关联点
程序计数器记录当前线程执行的字节码行号执行引擎通过它知道下条指令
栈(线程)存放栈帧,每个方法对应一个栈帧操作数栈执行字节码指令
本地方法栈执行 Native 方法图中连接 xx.dll
存放对象实例和数组new 的对象在这里
方法区(元空间)存储类元数据、常量池、静态变量常量池存放符号引用
直接内存NIO 使用的堆外内存-

2.2 栈帧结构详解(重点)

每个方法执行时都会创建一个栈帧,包含:

┌─────────────────────────────┐ │ 栈帧结构 │ ├─────────────────────────────┤ │ 局部变量表 (Local Variables) │ │ - 方法参数 │ │ - this 引用(实例方法) │ │ - 局部变量 │ ├─────────────────────────────┤ │ 操作数栈 (Operand Stack) │ │ - 字节码指令的工作区 │ │ - 压栈 (push) / 出栈 (pop) │ ├─────────────────────────────┤ │ 动态链接 (Dynamic Linking) │ │ - 指向运行时常量池的方法引用 │ ├─────────────────────────────┤ │ 方法返回地址 (Return Address)│ │ - 方法执行完毕后的返回位置 │ └─────────────────────────────┘ 

三、Java 程序的执行链路

3.1 完整执行流程

结合内存结构图,Java 程序的执行链路:

Hello.java ↓ javac 编译 Hello.class(字节码存储在方法区) ↓ 类加载器子系统加载 方法区中的类元数据 + 常量池 ↓ 执行引擎解释/JIT 编译 操作栈帧中的操作数栈 → 最终机器码 

3.2 关键认知

错误认知:Java 是解释型语言,直接运行 .java 文件
正确认知:Java 是"半编译半解释"语言,JVM 只识别字节码指令


四、实战:使用 javap 分析 class 文件

4.1 环境准备

创建测试类 Hello.java

publicclassHello{publicvoidsay(){System.out.println("hello");}publicstaticvoidmain(String[] args){newHello().say();}}

4.2 编译与反编译

# 编译生成 class 文件 javac Hello.java # 查看字节码指令(推荐) javap -c Hello # 查看完整结构(含常量池) javap -v Hello.class 

4.3 分析 say 方法的字节码

执行 javap -c Hello,得到:

publicvoidsay();Code:0: getstatic #2// Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #3// String hello5: invokevirtual #4// Method java/io/PrintStream.println:(Ljava/lang/String;)V8:return

指令解析

表格

偏移量指令操作数含义操作数栈变化
0getstatic#2从方法区获取静态字段System.out[][PrintStream]
3ldc#3从常量池加载字符串"hello"[PrintStream][PrintStream, "hello"]
5invokevirtual#4调用虚方法println弹出参数,执行方法
8return-方法返回清空当前栈帧
结论:一行简单的打印代码,JVM需要4条指令,核心逻辑是操作数栈的管理。

五、深入理解:基于栈的执行模型

5.1 操作数栈的执行过程

结合栈帧结构,图解4条指令的执行:

在这里插入图片描述
在这里插入图片描述

5.2 与内存结构的关联

  • 字节码指令:存储在方法区的类文件中
  • 执行过程:在栈帧的操作数栈中进行
  • 对象数据:存储在中,通过引用访问
  • 下条指令地址:由程序计数器维护

六、class文件结构解析

执行 javap -v Hello.class,观察class文件如何映射到内存结构:

Classfile/Users/demo/Hello.classLast modified 2024-01-15; size 417 bytes MD5 checksum 3e8f2b1c9d4e5f6a7b8c9d0e1f2a3b4c Compiled from "Hello.java" minor version:0 major version:52// Java 8 flags:(0x0021)ACC_PUBLIC,ACC_SUPER// 常量池(加载后进入方法区)Constant pool: #1=Methodref #6.#15// java/lang/Object."<init>":()V #2=Fieldref #16.#17// java/lang/System.out:Ljava/io/PrintStream; #3=String #18// hello #4=Methodref #19.#20// java/io/PrintStream.println:(Ljava/lang/String;)V// ... 其他常量// 方法表(字节码指令部分){publicvoidsay(); descriptor:()V flags:ACC_PUBLICCode: stack=2, locals=1, args_size=1// 操作数栈深度=2,局部变量1个0: getstatic #2 3: ldc #35: invokevirtual #48:returnLineNumberTable: line 3:0LocalVariableTable:StartLengthSlotNameSignature090thisLHello;}

6.1 class文件与内存映射

表格

class文件结构加载后内存区域作用
模数与版本方法区(元数据)标识文件类型和JDK版本
常量池方法区(运行时常量池)符号引用,动态链接使用
访问标志方法区类的修饰符
方法表方法区字节码指令,执行引擎读取
属性表方法区行号表、局部变量表等调试信息

七、JVM设计哲学:为什么采用字节码+栈模型?

7.1 三大核心优势

特性说明对应内存结构优势
平台无关性字节码与硬件无关统一的操作数栈抽象,屏蔽底层寄存器差异
延迟链接符号引用运行时解析常量池在方法区,支持动态加载
动态绑定invokevirtual运行时寻址栈帧的动态链接指向运行时常量池

7.2 多态的底层实现

思考以下代码:

`Animal animal =newDog(); animal.say();*// 调用Dog.say(),但字节码显示Animal.say*`

字节码层面

`invokevirtual #4*// Method Animal.say:()V*`

实际执行:执行引擎根据animal指向的堆中对象(Dog实例),查找方法表确定实际方法。

这种运行时绑定机制,正是JVM实现多态的关键,也是invokevirtual指令的设计精髓。


八、实践:常见代码的字节码模式

8.1 变量赋值

int a =10;
bipush 10*// 将10压入操作数栈* istore_1 *// 弹出栈顶,存入局部变量表slot 1*

8.2 算术运算

java复制

int b = a +20;
iload_1 *// 加载局部变量1(a)入栈* bipush 20*// 压入20* iadd *// 弹出两个int,相加后压回栈* istore_2 *// 结果存入局部变量2(b)*

8.3 对象创建(涉及堆)

Object obj =newString("test");
new #2*// 在堆中分配内存,创建String对象* dup *// 复制栈顶引用(用于构造函数和赋值)* ldc #3*// 加载"test"入栈* invokespecial #4*// 调用String构造方法* astore_1 *// 存入局部变量表*

九、总结与展望

本文通过 System.out.println 案例,结合JVM内存结构图,揭示了:

  1. 执行链路:源码→字节码→机器码,JVM只识别字节码指令
  2. 栈式执行:所有操作围绕栈帧操作数栈展开
  3. 内存布局:class文件加载到方法区,对象创建在,执行在
  4. 设计优势:跨平台、延迟链接、动态绑定支撑了Java生态

下篇预告:《JVM方法调用深度解析:invokevirtual、invokespecial、invokestatic的区别与实现》


如果觉得本文对你有帮助,欢迎:

  • 👍 点赞:让更多人看到优质内容
  • 收藏:建立JVM知识体系
  • 💬 评论:交流技术心得
  • 🔗 分享:帮助更多开发者成长

参考资料

Read more

Java外功精要(6)——Spring事务及其传播机制

Java外功精要(6)——Spring事务及其传播机制

1.概述 Spring事务管理是Spring框架中用于确保数据库操作 原子性、一致性、隔离性和持久性(ACID) 的核心机制。它通过声明式或编程式(本文略)方式管理事务,支持多种事务传播行为和隔离级别相较于编程式事务,声明式事务通过@Transactional注解实现事务管理,无需手动编写事务代码事务基本概念在全面解析MySQL(5)——“索引、事务、JDBC”三大核心一文中有介绍,本文不再赘述 2.@Transactional 作用:提供声明式事务管理。它简化了在应用程序中管理数据库事务的流程。开发者只需在方法或类上添加此注解,Spring框架就会自动处理事务的开启、提交和回滚,无需手动编写事务管理代码(如 begin、commit、rollback) 级别:类 + 方法作为类注解:为类中所有public方法添加注解作为方法注解:默认仅对public方法生效 @RequestMapping("/test")@RestController@Slf4jpublicclassTestController{privatefinalUserService userService;@A

By Ne0inhk
【Linux/C++多进程篇(一) 】一个变两个?揭秘 C/C++ 程序中神奇的“分身术”

【Linux/C++多进程篇(一) 】一个变两个?揭秘 C/C++ 程序中神奇的“分身术”

⭐️在这个怀疑的年代,我们依然需要信仰。 个人主页:YYYing. ⭐️Linux/C++进阶系列专栏:【从零开始的linux/c++进阶编程】 ⭐️其他专栏:【linux基础】【数据结构与算法】【从零开始的计算机网络学习】 系列上期内容:【Linux/C++文件篇(一) 】标准I/O与文件I/O基础  系列下期内容:【Linux/C++多进程篇(二) 】万字解析linux系统编程之进程间通信 (IPC) 目录 前言:        多进程理论基础 一、为什么要引入多进程 二、多进程相关概念 三、进程的内存管理 四、进程与程序的区别 五、进程的种类 六、进程PID 七、特殊的进程 八、linux中有关进程的指令 九、进程状态的切换

By Ne0inhk
Java外功精要(2)——Spring IoC&DI

Java外功精要(2)——Spring IoC&DI

1.IoC(控制反转) 1.1 Spring Ioc VS Servlet 在上文:Java外功基础(1)——Spring Web MVC中,很形象地模拟出使用Spring"建造房子"的大概流程。使用Spring建造房子不需要像Servlet那样烧制每一块砖,只需要从Spring中取出一个个提前预制好的组件然后组装即可。换言之:Spring是包含了大量工具的IoC容器 1.2 IoC解析 1.2.1 IoC概述 概念:IoC(Inversion of Control,控制反转),是一种设计原则,用于减少代码间的直接依赖关系。传统编程中,调用者通常主动创建和管理被调用者的生命周期,而 IoC 将这种控制权交给外部容器或框架,由容器负责对象的创建、依赖注入和管理 示例一:传统编程模式 classCar{protectedFramework

By Ne0inhk

汉化版IDEA 更换 JDK 版本全教程,超详细!

汉化版IDEA 更换 JDK 版本全教程,超详细! 在 Java 开发中,我们经常需要根据项目需求切换不同的 JDK 版本(比如从 JDK8 升级到 JDK17,或降级到 JDK11)。对于使用汉化版 IntelliJ IDEA的小伙伴,本文全程采用中文菜单 / 选项描述,不改动任何非必要配置,手把手教你完成 JDK 切换,全程避坑! 一、场景 1:设置全局默认 JDK(所有新项目生效) * 若未打开任何项目:直接点击 IDEA 启动界面的「文件 → 新项目设置 → 新项目的结构」(2020 + 版本); * 若已打开项目:点击「文件 → 项目结构」。 1. 在弹出的「项目结构」窗口左侧,

By Ne0inhk