【JVM】从i++到JVM栈帧

【JVM】从i++到JVM栈帧

本篇博客将用两个代码例子,简单认识一下JVM与栈帧结构以及其作用

从i++与++i说起

先不急着看i++和++i,我们来看看JVM虚拟机(请看VCR.JPG)

我们初学JAVA的时候一定都听到过JAVA“跨平台”的特性,也就是说,我们的代码可以直接在windows上运行,也可以直接在Linux上运行,而这种特性与虚拟机(也就是JVM)息息相关。

image-20240426203210582

那么JVM这么强大,其内部内存结构是怎样的?笔者将在后面向大家简单介绍。

光知道跨平台与JVM息息相关,便足够了吗?

​ ——鲁迅

正如鲁迅所说,写程序不能只知其然而不知其所以然,我们来看看JVM的内存模型(运行时数据区)。

image-20240426205754646

由于篇幅限制(其实是笔者没学过),本篇博客将会暂时忽略其中的部分内容,重点将会放在虚拟机栈,即栈帧的介绍中。

浅析运行时数据区各个功能区的作用

1. 堆(Heap)

堆是JVM中最大的一块内存区域,主要用于存储所有类实例(对象)和数组。

2. 方法区(Method Area)

方法区是所有线程共享的内存区域,用于存储每个类的结构如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容。它类似于永久代,但是随着时间的推移,它已经和永久代分开,并且可以是堆的一个逻辑部分,也可以是虚拟机自己的内存(非堆)。

3. 虚拟机栈(Stack)

Java虚拟机的栈是一组私有的执行栈,每个线程有自己的执行栈。每个方法执行的同时都会创建一个栈帧用于存储局部变量表, 操作数栈, 动态链接, 方法返回等信息。一个 Java 方法从调用到执行完的过程, 就对应着一个栈帧从虚拟机栈入栈到出栈的过程。

4. 程序计数器(Program Counter Register)

程序计数器是一块较小的内存空间,它存储当前线程正在执行的字节码的地址,即当前指令的地址。

5. 本地方法栈(Native Method Stack)

本地方法栈用于存储本地方法(如用C或C++编写的方法)的局部变量和参数。它与Java栈类似,其区别只是虚拟机栈为 JVM 执行 Java 方法服务, 而本地方法栈则是为虚拟机使用到的本地 (Native) 方法服务

栈帧

还记得我们刚刚略过的i++吗,现在回到正题,我们都知道i++与++i的机制:

  • i++:首先返回i的当前值,然后i的值加1。
  • ++i:先将i的值加1,然后返回新值。

为了理解i++++i在JVM层面的实现,我们需要了解JVM栈帧的结构。当一个方法被调用时,JVM为这个方法创建一个栈帧,它包含了以下关键部分:

  1. 局部变量表
  2. 操作数栈
  3. 动态链接
  4. 方法返回地址
  5. 附加信息

我们先简单从字面意义上了解一下前两个部分的作用。

局部变量表:主要用于存储方法参数和定义在方法体内的局部变量。

操作数栈:主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。相当于一个工作区。

以下内容配图观看(以i++为例):

image-20240426212511587

而后压一个1进入操作数栈中,执行加一操作。

image-20240426212552157

最后将i值赋值回局部变量表中。

image-20240426212635740

i++

对于后缀递增操作,JVM需要先提供变量i的当前值,然后再将其值增加一。在栈帧层面,这通常意味着以下步骤:

  1. 加载变量:将变量i的值加载到操作数栈上。
  2. 返回当前值:由于需要返回当前值,因此这一步加载的值就是表达式i++的值。
  3. 增加一:将一个常量1推送到操作数栈上,然后执行iadd(整数加法)指令,将i的值与1相加。
  4. 存储结果:将加法的结果存储回局部变量表中的i

在字节码层面,可能是这样:

iload_0       // 加载局部变量i到栈上
dup            // 复制栈顶的值(i的当前值)
iconst_1       // 将常量1压入栈
iadd           // 执行加法操作,栈顶的两个整数相加
istore_0       // 将相加的结果存储回局部变量i

++i

对于前缀递增操作,JVM首先将i的值增加一,然后再提供新值。这在字节码层面的实现通常如下:

  1. 加载变量:将变量i的值加载到操作数栈上。
  2. 增加一:将一个常量1推送到操作数栈上,然后执行iadd指令,将i的值与1相加。
  3. 存储结果:将加法的结果存储回局部变量表中的i
  4. 返回新值:此时,操作数栈上的值已经是增加后的值,可以直接使用。

对应的字节码可能如下:

iload_0       // 加载局部变量i到栈上
iconst_1       // 将常量1压入栈
iadd           // 执行加法操作
istore_0       // 将相加的结果存储回局部变量i

Java栈帧

小栗子(finally)

我们再来看一个小栗子:

image-20240426213609990

不是这个,是这个:

image-20240426213636409

注:这段代码示例借鉴自:

【Java】异常基本知识点-CSDN博客

我们知道try-catch-finally模板主要用于异常的抓取,如果try块中识别到异常,则执行catch块中的代码,而无论有无异常,都会执行finally块。

这段代码首先将10传入number方法中,而后进入try语句块,发现并未捕捉到异常,正想要返回,却发现还有个finally语句,于是跳转至finally语句,执行两行语句之后,欣然返回main方法。

目前看来,执行的顺序大概是这样:

image-20240426214419185

这时问题出现了,最后print出的num应该是多少?如果从执行顺序来看,似乎应该是11,毕竟是先执行的++num,再运行的return num。

不卖关子,直接运行。

image-20240426214110587

这是为什么呢?疑惑龙.jpg:

image-20240426214543911

掏出我们刚学的栈帧,来看看这段代码:

main方法的栈帧
  1. 方法调用main方法调用number(10)方法。
  2. 创建栈帧:JVM为number方法创建一个新的栈帧。
  3. 参数传递number方法的参数num被设置为10,并存储在新栈帧的局部变量表中。
number方法的栈帧
  1. 局部变量表num的初始值设置为10。
  2. 操作数栈System.out.println("开始")执行,将字符串"开始"推送到操作数栈上,随后调用系统输出方法。
  3. 返回值return num;执行,num的值(10)被推送到操作数栈上,准备返回给main方法。
catch

尽管number方法中有一个try-catch语句,但在这个例子中,并没有抛出NumberFormatException,因此catch块不会被执行。

finally
  1. 执行finally:无论是否发生异常,finally块中的代码都会执行。
  2. 局部变量更新++num;执行,num的值从10增加到11,但这个改变只在number方法的栈帧中有效。
返回main方法
  1. 方法返回number方法返回,操作数栈上的返回值(10)传递给main方法。
  2. 输出结果main方法中的System.out.println使用这个返回值打印"num = 10"。
  3. finally块完成:尽管main方法已经接收到返回值,但number方法的finally块仍然会执行,输出"运行完成"。

正式介绍一下栈帧

由于笔者才疏学浅,对动态链接具体的机制不甚清楚,故而放到日后的博客中详细记述。

把上面的栈帧结构搬过来~

  1. 局部变量表(Local Variables)
  2. 操作数栈(Operand Stack)
  3. 动态链接(Dynamic Linking)
  4. 方法返回地址(Return Address)
  5. 附加信息

img

局部变量表

局部变量表也被称为局部变量数组或本地变量表。

定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量。

操作数栈

操作数栈,也可称为表达式栈,它是存放字节码操作指令的栈,存在于每一个独立的栈帧中。

在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)。

某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们之后再把结果压入栈。

比如:执行复制、交换、求和等操作。

注:

· 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

· 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。

· 操作数栈并非采用访问索引的方式来进行数据访问,而是只能通过标准的入栈和出栈操作来完成一次数据访问。

动态链接

每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。

在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里

比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

方法返回地址

存储下一条指令的地址:当一个方法被调用时,JVM会将调用下一条指令的地址压入调用者方法的栈帧中。这个地址是方法调用后应该继续执行的地方。

方法返回时恢复执行:当方法执行完毕并准备返回时,JVM会从当前栈帧中弹出返回地址,并将其放入程序计数器(Program Counter Register)中。这样,当控制权从当前方法返回到调用者方法时,JVM就知道接下来应该执行哪一条指令。

方法返回的两种方式:

1)执行引擎遇到任意一个方法返回的指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口

2)在方法执行的过程中遇到了异常(Exception),如果有异常处理器,则交给异常处理器,如果无,则抛出异常简称异常完成出口

结语

笔者对栈帧,对JVM的理解仍有许多不足之处,光是落笔的当下,就会生出许多问题,例如:动态链接机制是如何实现的?是否存在静态链接?GC垃圾回收机制如何实现,何为新生代和老生代?期待在日后的学习中,能自己一一解决。

参考资料:

一起来边打扫卫生边学习JVM运行时数据区_哔哩哔哩_bilibili

JVM-用栈帧来炼制i++丹药_哔哩哔哩_bilibili

Java高级面试题:栈帧结构以及动态链接是什么?_哔哩哔哩_bilibili

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/3930.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

18 JavaScript学习:错误

JavaScript错误 JavaScript错误通常指的是在编写JavaScript代码时发生的错误。这些错误可能是语法错误、运行时错误或逻辑错误。以下是对这些错误的一些常见分类和解释: 语法错误: 这类错误发生在代码编写阶段,通常是由于代码不符合JavaScrip…

Linux常用指令001

实验案例 创建一个和你名字同名的用户 在当前目录下创建名称为 1212的目录 进入到 1212 目录中 创建 a~d 目录 创建 1~10.txt 文件,如下 备份 创建一个和 1212 同一级目录的新目录 1313 将所有的文件和目录备份到 1313 目录中 在 1313目录中,查看…

小程序中如何快速给分类添加商品

​快速在分类下面上传商品,并且能够设置商品顺序,关系到运营效率的高低。下面就具体介绍如何快速在某个分类下面设置商品。 一、在商品管理处,查询某个分类下面的商品。 进入小程序管理员后台->商品管理,点击分类输入框&…

Xilinx 7系列中clock IP核通过AXI4-Lite接口实现动态重新配置

当选择了动态重配置(Dynamic Reconfiguration)选项时,AXI4-Lite接口将默认被选中用于重新配置时钟组件。动态重新配置可以通过AXI4-Lite接口实现了Clocking Wizard IP核的时钟组件MMCM/PLL的动态重新配置。 如果需要直接访问MMCM/PLL的DRP寄…

基于LSTM算法实现交通流量预测(Pytorch版)

算法介绍 LSTM(Long Short-Term Memory)算法是一种特殊设计的循环神经网络(RNN, Recurrent Neural Network),专为有效地处理和建模序列数据中的长期依赖关系而开发。由于传统RNN在处理长序列时容易遇到梯度消失和梯度…

Linux驱动开发——(七)Linux阻塞和非阻塞IO

目录 一、阻塞和非阻塞IO简介 二、等待队列 2.1 等待队列头 2.2 等待队列项 2.3 将队列项添加/移除等待队列头 2.4 等待唤醒 2.5 等待事件 三、轮询 四、驱动代码 4.1 阻塞IO 4.2 非阻塞IO 一、阻塞和非阻塞IO简介 IO指的是Input/Output,也就是输入/输…

如何解决冲突性需求,看看TRIZ怎么做

​本田公司的产品经理(本田的产品经理被称为是大型产品领导人,large product leader)在设计第三代雅阁的时候,面临的需求主要集中在三个方面:1、视野要好;2、空间要大;3、发动机要强劲。 每一个…

TCP关闭连接时的一些思考

TCP协议是TCP/IP栈中最复杂的协议,它最大的优点是传输的可靠性,这通过面向连接、按序传输、超时重传、流量控制等机制保证其传输的可靠性。但这并不是我们今天要讨论的重点! TCP通信的过程分别是三个阶段:建立连接、传输数据、关…

图论基础知识 深度优先(Depth First Search, 简称DFS),广度优先(Breathe First Search, 简称DFS)

图论基础知识 学习记录自代码随想录 dfs 与 bfs 区别 dfs是沿着一个方向去搜,不到黄河不回头,直到搜不下去了,再换方向(换方向的过程就涉及到了回溯)。 bfs是先把本节点所连接的所有节点遍历一遍,走到下…

从单按键状态机思维扫描引申到4*4矩阵按键全键无冲扫描,一步一步教,超好理解,超好复现(STM32程序例子HAL库)

目前大部分代码存在的问题 ​ 单次只能对单个按键产生反应;多个按键按下就难以修改;并且代码耦合度较高,逻辑难以修改,对于添加长按,短按,双击的需求修改困难。 解决 16个按键按下无冲,并且代…

如何在CentOS本地搭建DataEase数据分析服务并实现远程查看数据分析

文章目录 前言1. 安装DataEase2. 本地访问测试3. 安装 cpolar内网穿透软件4. 配置DataEase公网访问地址5. 公网远程访问Data Ease6. 固定Data Ease公网地址 前言 DataEase 是开源的数据可视化分析工具,帮助用户快速分析数据并洞察业务趋势,从而实现业务…

【项目分享】用 Python 写一个桌面倒计日程序!

事情是这样的,我们班主任想委托我做一个程序,能显示还有几天考试。我立即理解了这个意思,接下了这个项目。 话不多说,来看看这个项目吧—— 项目简介 仓库地址:https://gitee.com/yaoqx/desktop-countdown-day 这是 …

幻兽帕鲁中文怎么设置 游戏中文修改方法 《幻兽帕鲁》宠物指定配种显示英文解决方法 幻兽帕鲁Steam游戏解说合集 Mac玩Windows游戏

在广阔的世界中收集神奇的生物“帕鲁”,派他们进行战斗、建造、做农活,工业生产等,这是一款支持多人游戏模式的全新开放世界生存制作游戏。幻兽帕鲁支持多人在线捕捉“帕鲁”,展开丰富的冒险玩法;不同的关卡具有不同的…

Bellman Ford算法:解决负权边图的最短路径问题

Bellman Ford算法的介绍 在计算机科学的世界中,Bellman Ford算法是一种解决单源最短路径问题的算法,它可以处理有负权边的图。这个算法的名字来源于两位科学家Richard Bellman和Lester Randolph Ford,他们是这个算法的发明者。 这个算法的主…

AI图书推荐:2024年ChatGPT副业搞钱指南

本书《2024年ChatGPT副业搞钱指南》(ChatGPT Side Hustles 2024)由Alec Rowe撰写,旨在指导读者如何利用ChatGPT技术来提升被动收入、创造新的现金流,并在数字化时代保持领先。 本书是深入了解被动收入未来的综合指南。本书揭示了超…

【算法基础实验】图论-基于DFS的连通性检测

基于DFS的连通性检测 理论基础 在图论中,连通分量是无向图的一个重要概念,特别是在处理图的结构和解析图的组成时。连通分组件表示图中的一个子图,在这个子图中任意两个顶点都是连通的,即存在一条路径可以从一个顶点到达另一个顶…

Flutter应用下拉菜单设计DropdownButtonFormField控件介绍

文章目录 DropdownButtonFormField介绍使用方法重点代码说明属性解释 注意事项 DropdownButtonFormField介绍 Flutter 中的 DropdownButtonFormField 是一个用于在表单中选择下拉菜单的控件。它是 DropdownButton 和 TextFormField 的组合,允许用户从一组选项中选择…

井字棋游戏

1. 游戏创建 1.1导包 from tkinter import * import numpy as np import math import tkinter.messagebox 1.2 窗口内容 1.2.1创建一个窗口 root Tk() # 窗口名称 root.title("井字棋 from Sun") 1.2.2 创建一个框架,将其放置在窗口中 Frame1 F…

汽车底盘域的学习笔记

前言:底盘域分为传统车型底盘域和新能源车型底盘域(新能源系统又可以分为纯电和混动车型,有时间可以再研究一下) 1:传统车型底盘域 细分的话可以分为四个子系统 传动系统 行驶系统 转向系统 制动系统 1.1传动系…

什么样的内外网文档摆渡,可以实现安全高效传输?

内外网文档摆渡通常指的是在内网(公司或组织的内部网络)和外网(如互联网)之间安全地传输文件的过程。这个过程需要特别注意安全性,因为内网往往包含敏感数据,直接连接内网和外网可能会带来安全风险。因此会…