三言两语:JVM 字节码执行实例分析

2024-04-10 13:18

本文主要是介绍三言两语:JVM 字节码执行实例分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言
最近在看《Java 虚拟机规范》和《深入理解JVM虚拟机》,对于字节码的执行有了进一步的了解。字节码就像是汇编语言,是 JVM 的指令集。下面我们先对 JVM 执行引擎做一下简单介绍,然后根据实例分析 JVM 字节码的执行过程。包括:

for 循环字节码分析
try-catch-finally 字节码分析
运行时栈帧结构
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

在编译程序员代码的时候,栈帧中局部变量表和操作数栈的大小已经确定了,并且写入到方法表中的Code属性中。

在活动线程中,只有位于栈顶的栈帧才是有效的, 称为当前栈帧,与这个栈帧关联的方法称为当前方法。执行引擎运行的所有字节码指令只对当前栈帧进行操作。

局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量表的容量以变量槽(slot)为最小单位,每个 slot 保证能放下 32 位内的数据类型。虚拟机通过索引定位的方式使用局部变量表,索引值从 0 开始。值得注意的是,对于实例方法,局部变量表中第 0 位索引的 slot 默认是this引用;静态方法则不是。而且为了节约内存,slot 是可以重用的。

操作数栈
操作数栈的元素可以是任意的 Java 数据类型。当一个方法开始时,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈入栈操作。

实例分析
下面分析的字节码指令主要是对局部变量表和操作栈的读写。

for 循环字节码分析
void spin() {
int i;
for (i = 0; i < 100; i++) {
; // Loop body is empty
}
}
上面是一个空循环的代码,编译后的字节码如下:

Method void spin()
0 iconst_0 // Push int constant 0
1 istore_1 // Store into local variable 1 (i=0)
2 goto 8 // First time through don’t increment
5 iinc 1 1 // Increment local variable 1 by 1 (i++)
8 iload_1 // Push local variable 1 (i)
9 bipush 100 // Push int constant 100
11 if_icmplt 5 // Compare and loop if less than (i < 100)
14 return // Return void when done
相信大家看到上面的代码都是一脸懵逼,即使有注释还是不知道字节码到底做了什么操作。下面我就图解每一条指令,帮助理解。上面的代码都是对局部变量表和操作数栈的操作,所以我们的关注点就在这两个区域上。(栈是自顶向下的)

0 iconst_0 //把常量0放入栈
±-------±-------+
| local | stack |
±----------------+
| | 0 |
±----------------+
| | |
±-------±-------+

1 istore_1 //把栈顶的元素出栈,存到局部变量表索引为1的位置
±-------±-------+
| local | stack |
±----------------+
| 0 | |
±----------------+
| | |
±-------±-------+

2 goto 8 //跳转到第8条指令

8 iload_1 //把局部变量表中索引为1的变量入栈
±-------±-------+
| local | stack |
±----------------+
| 0 | 0 |
±----------------+
| | |
±-------±-------+

9 bipush 100 //把100入栈
±-------±-------+
| local | stack |
±----------------+
| 0 | 0 |
±----------------+
| | 100 |
±-------±-------+

11 if_icmplt 5 //出栈两个元素v1,v2,比较它们的值,当且仅当v1 < v2,跳转到指令5
±-------±-------+
| local | stack |
±----------------+
| 0 | |
±----------------+
| | |
±-------±-------+

5 iinc 1 1 //自增局部变量表中索引为1的值
±-------±-------+
| local | stack |
±----------------+
| 1 | |
±----------------+
| | |
±-------±-------+

//进行下次循环直到指令11不满足,到达指令14
14 return //清空栈,执行引擎把控制权交换给调用者。
±-------±-------+
| local | stack |
±----------------+
| 100 | |
±----------------+
| | |
±-------±-------+
以上就是for循环字节码执行的过程。可以发现,所有指令都是围绕者局部变量表和操作数栈在操作。

解惑
指令iconst_0,iload_1的命名解读
第一个i代表这是对int数据类型进行的操作
const,load是操作码
0,1是隐含的操作数
上面的两个指令等价于iconst 0,iload 1
详细的字节码解释查阅《JVM 虚拟机规范》

try-catch-finally 字节码分析
static int inc(){
int x;
try {
x = 1;
return x;
} catch (Exception e){
x = 2;
return x;
} finally {
x = 3;
}
}
下面是它的字节码,这次我就不画图了,里面的命令跟上面的类似。

static int inc();
descriptor: ()I
flags: ACC_STATIC
Code:
stack=1, locals=4, args_size=0
0: iconst_1 //try 块中的 x = 1;
1: istore_0 //保存栈顶元素到局部变量表中索引为 0 的 slot 中
2: iload_0 //加载局部变量表中索引为 0 的值到栈中
3: istore_1 //保存栈顶元素到局部变量表中索引为 1 的 slot 中
4: iconst_3 //finally 块中的 x = 3;
5: istore_0 //保存栈顶元素到局部变量表中索引为 0 的 slot 中,x 的值存在这里。
6: iload_1 //加载局部变量表中索引为 1 的值到栈中
7: ireturn //返回栈顶元素,即 x = 1;正常情况下函数运行到这里就结束了,如果出现异常根据异常表跳转到指定的位置
8: astore_1 //给 catch 块中定义的 Exception e 赋值,存储在 slot1 中。
9: iconst_2 //catch 块中的 x = 2;
10: istore_0
11: iload_0
12: istore_2
13: iconst_3 //finally 块中的 x = 3;
14: istore_0
15: iload_2
16: ireturn //此时返回的是 slot2 中的值,即 x = 2
17: astore_3 //如果出现不属于 java.lang.Exception 及其子类的异常,才会根据异常表中的规则跳转到这里。
18: iconst_3 //finally 块中的 x = 3;
19: istore_0
20: aload_3 //将异常加载到栈顶,
21: athrow //抛出栈顶的异常
Exception table:
from to target type
0 4 8 Class java/lang/Exception
0 4 17 any
8 13 17 any
字节码中 0 ~ 4 行将整数 1 赋值为变量 x,x 存储在 slot0 中,并且将 x 的值拷贝一份放到 slot1。如果没有出现异常,继续走到 5 ~ 7 行,将 x 赋值为 3,然后读取 slot1 中的值到栈顶,最后ireturn返回栈顶的值,方法结束。
如果出现异常,PC 寄存器指针转到第 8 行,第 8 ~ 16 行所做的事情就是将 2 赋值给 x,然后保存 x 的拷贝,最后将 x 赋值为 3。方法返回前将 x 的拷贝 2 读取到栈顶。
如果在 0 ~ 4,8 ~ 13 行中出现其他异常,则跳转到第 17 行执行,先同样执行finally块中的x = 3,最后抛出异常,方法结束。
可以看到,Java 字节码是通过异常表的方式来决定代码执行的路径。而finally的实现是通过在每个路径的最后加入finally块中的字节码实现的。
深圳网站建设www.sz886.com

这篇关于三言两语:JVM 字节码执行实例分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/891170

相关文章

Java Stream流与使用操作指南

《JavaStream流与使用操作指南》Stream不是数据结构,而是一种高级的数据处理工具,允许你以声明式的方式处理数据集合,类似于SQL语句操作数据库,本文给大家介绍JavaStream流与使用... 目录一、什么是stream流二、创建stream流1.单列集合创建stream流2.双列集合创建str

springboot集成easypoi导出word换行处理过程

《springboot集成easypoi导出word换行处理过程》SpringBoot集成Easypoi导出Word时,换行符n失效显示为空格,解决方法包括生成段落或替换模板中n为回车,同时需确... 目录项目场景问题描述解决方案第一种:生成段落的方式第二种:替换模板的情况,换行符替换成回车总结项目场景s

SpringBoot集成redisson实现延时队列教程

《SpringBoot集成redisson实现延时队列教程》文章介绍了使用Redisson实现延迟队列的完整步骤,包括依赖导入、Redis配置、工具类封装、业务枚举定义、执行器实现、Bean创建、消费... 目录1、先给项目导入Redisson依赖2、配置redis3、创建 RedissonConfig 配

SpringBoot中@Value注入静态变量方式

《SpringBoot中@Value注入静态变量方式》SpringBoot中静态变量无法直接用@Value注入,需通过setter方法,@Value(${})从属性文件获取值,@Value(#{})用... 目录项目场景解决方案注解说明1、@Value("${}")使用示例2、@Value("#{}"php

SpringBoot分段处理List集合多线程批量插入数据方式

《SpringBoot分段处理List集合多线程批量插入数据方式》文章介绍如何处理大数据量List批量插入数据库的优化方案:通过拆分List并分配独立线程处理,结合Spring线程池与异步方法提升效率... 目录项目场景解决方案1.实体类2.Mapper3.spring容器注入线程池bejsan对象4.创建

线上Java OOM问题定位与解决方案超详细解析

《线上JavaOOM问题定位与解决方案超详细解析》OOM是JVM抛出的错误,表示内存分配失败,:本文主要介绍线上JavaOOM问题定位与解决方案的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录一、OOM问题核心认知1.1 OOM定义与技术定位1.2 OOM常见类型及技术特征二、OOM问题定位工具

基于 Cursor 开发 Spring Boot 项目详细攻略

《基于Cursor开发SpringBoot项目详细攻略》Cursor是集成GPT4、Claude3.5等LLM的VSCode类AI编程工具,支持SpringBoot项目开发全流程,涵盖环境配... 目录cursor是什么?基于 Cursor 开发 Spring Boot 项目完整指南1. 环境准备2. 创建

Spring Security简介、使用与最佳实践

《SpringSecurity简介、使用与最佳实践》SpringSecurity是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架,本文给大家介绍SpringSec... 目录一、如何理解 Spring Security?—— 核心思想二、如何在 Java 项目中使用?——

SpringBoot+RustFS 实现文件切片极速上传的实例代码

《SpringBoot+RustFS实现文件切片极速上传的实例代码》本文介绍利用SpringBoot和RustFS构建高性能文件切片上传系统,实现大文件秒传、断点续传和分片上传等功能,具有一定的参考... 目录一、为什么选择 RustFS + SpringBoot?二、环境准备与部署2.1 安装 RustF

springboot中使用okhttp3的小结

《springboot中使用okhttp3的小结》OkHttp3是一个JavaHTTP客户端,可以处理各种请求类型,比如GET、POST、PUT等,并且支持高效的HTTP连接池、请求和响应缓存、以及异... 在 Spring Boot 项目中使用 OkHttp3 进行 HTTP 请求是一个高效且流行的方式。