深入理解java虚拟机:虚拟机字节码执行引擎(1)

news2025/7/22 6:52:10

文章目录

  • 1. 概述
  • 2. 运行时栈帧结构
    • 2.1 局部变量表
    • 2.2 操作数栈
    • 2.3 动态连接
    • 2.4 方法返回地址
    • 2.5 附加信息

1. 概述

代码编译的结果是从 本地机器码 转变为 字节码 ,是存储格式发展的一小步,却是编程语言发展的一大步。

执行引擎 是Java虚拟机最核心的组成部分之一。“虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,而虚拟机的执行引擎则是由自己实现的,因此可以自行制定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。

在Java虚拟机规范中制定了 虚拟机字节码执行引擎 的概念模型,这个概念模型成为各种虚拟机执行引擎的统一外观(Facade)。在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能有 解释执行(通过解释器执行)编译执行(通过即时编译器产生本地代码执行)两种选择,也可能两者兼备,甚至还可能包含几个不同级别的编译器执行引擎。但从外观上看起来,所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果

2. 运行时栈帧结构

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的 虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的 局部变量表操作数栈动态连接方法返地址附加信息。每一个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

编译 程序代码的时候,栈帧中需要多大的局部变量表、多深的操作数栈都已经完全确定了,并且写入到方法表的 Code属性 之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于执行引擎来讲,活动线程中,只有栈顶的栈帧是有效的,称为 当前栈帧(CurrentStack Frame),这个栈帧所关联的方法称为 当前方法(Current Method)。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作,栈帧的概念结构如图所示

在这里插入图片描述

2.1 局部变量表

它是一组变量值存储空间,用于存放 方法参数 和方法内部定义的 局部变量。在Java程序被编译为Class文件时,就在方法的 Code属性max_locals数据项中确定了该方法所需要分配的最大局部变量表的容量

局部变量表的容量以 **变量槽(Variable Slot,下称Slot)**为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只是很有“导向性”地说明每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,这种描述与明确指出“每个Slot占用32位长度的内存空间”有一些差别,它允许Slot的长度随着处理器、操作系统或虚拟机的不同而发生变化。不过无论如何,即使在64位虚拟机中使用了64位长度的内存空间来实现一个Slot,虚拟机仍要使用对齐和补白的手段让Slot在外观上看起来与32位虚拟机中的一致。reference(可能是32位也可能是64位)和returnAddress的做法法与“long和double的非原子性协定”中把一次long和double数据类型读写分割为两次32位读写的做法类似。不过,由于 局部变量表建立在线程的堆栈 上,是 线程私有 的数据,无论读写两个连续的Slot是否是原子操作,都不会引起数据安全问题”。

在方法执行时,虚拟机是使用局部变量表完成 参数值到参数变量列表的传递 过程的,如果是实例方法(非static的方法),那么局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字**this** 来访问这个隐含的参数。其余参数则按照参数表的顺序来排列,占用从1开始的局部变量Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。

局部变量表中的Slot是可重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那么这个变量对应的Slot就可以交给其他变量使用。这样的设计不仅仅是为了节省栈空间,在某些情况下Slot的复用会直接影响到系统的垃圾收集行为,如下所示

public class Test {
    public static void main(String[] args)  {
		byte[] placeholder = new byte[64 * 1024 * 1024];
        System.gc();
    }
}
/*
java -verbose:gc Test

[0.010s][info][gc] Using G1
[0.087s][info][gc] GC(0) Pause Full (System.gc()) 67M->65M(224M) 2.281ms
*/

这里没回收,说的过去,因为执行Full GC的时候,变量 placeholder 还处于作用域中,虚拟机自然不敢回收这部分内存,我们改一下再看看

public class Test {
    public static void main(String[] args)  {
        {
          byte[] placeholder = new byte[64 * 1024 * 1024];
        }
        System.gc();
    }
}
/*
java -verbose:gc Test

[0.011s][info][gc] Using G1
[0.088s][info][gc] GC(0) Pause Full (System.gc()) 67M->65M(224M)  1.809ms
*/

加入了花括号之后,placeholder的作用域被限制在花括号之内,从代码逻辑上讲,在执行Full GC的时候,placeholder 已经不可能再被访问了,但执行一下这段程序,还是有65MB的内存没有被回收掉,这又是为什么呢?我们再修改一下看看:

public class Test {
    public static void main(String[] args)  {
        {
          byte[] placeholder = new byte[64 * 1024 * 1024];
        }
        int a=0;
        System.gc();
    }
}
/*
java -verbose:gc Test

[0.011s][info][gc] Using G1
[0.093s][info][gc] GC(0) Pause Full (System.gc()) 67M->0M(8M) 6.907ms
*/

这里的修改看起来很奇怪,但是运行后发现placeholder的内存被回收了, 能否被回收的根本原因就是:局部变量表中的 Slot 是否还存有关于placeholder数组对象的引用。第一次修改中,代码虽然已经离开了placeholder的作用域,但在此之后,没有任何对局部变量表的读写操作,placeholder原本所占用的SIot还没有被其他变量所复用,所以作为GC Roots一部分的局部变量表仍然保持着对它的关联。这种关联没有被及时打断,在绝大部分情况下影响都很轻微。但如果遇到一个方法,其后面的代码有一些耗时很长的操作,而前面又定义了占用了大量内存、实际上已经不会再被使用的变量,手动将其设置为null值(用来代替“int a=0;”,把变量对应的局部变量表Slot清空) 就不是一个毫无意义的操作,这种操作可以作为一种在极特殊情形(对象占用内存大、此方法的栈帧长时间不能被回收、方法调用次数达不到JIT的编译条件)下的“奇技”来使用。但不应当对赋null值操作有过多的依赖,也没有必要把它当做一个普遍的编码方法来推广。以恰当的变量作用域来控制变量回收时间才是最优雅的解决方法,上述代码不常见。

2.2 操作数栈

也被称为 操作栈,后入先出(Last In First Out,LIFO)栈。同局部变量表一样,操作栈的最大深度也在编译时期被写入到 Code属性max_stacks数据项之中。

操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。

2.3 动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。通过前面 深入理解java虚拟机:类文件结构(2),我们知道Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。

2.4 方法返回地址

当一个方法被执行后,有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个 方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为 正常完成出口(Normal Method InvocationCompletion)

另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用**athrow字节码指令**产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为 异常完成出口(Abrupt Method Invocation Completion)

无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈顿中一般不会保存这部分信息。

方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。

2.5 附加信息

虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与 调试相关的信息,这部分信息完全取决于具体的虚拟机实现,这里不再详述。在实际开发中,一般会把 动态连接方法返回地址 与其他 附加信息 全部归为一类,称为 栈帧信息

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

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

相关文章

pmp是什么意思啊?

PMP是一个证书,项目管理类的专业认证考试,从国外引进大陆已经很多年了,反响也不错。 以前,大陆每年报考PMP的人很少,那时的思维观念,更多的认为有了这个PMP证书,代表着你很上进,学习…

Terraform 初始化慢~配置本地离线源

解决Terraform初始化慢~配置本地离线源 - 知乎 这里不再介绍Terraform是啥了,可以参考最近上线的课程。直奔主题,配置一个离线的源。需要手动或者terraform init一次下载, 然后缓存。后续直接使用缓存。 本次实践使用的是Linux/Mac 系统&am…

【App自动化测试】(十二)App异常弹框处理

目录1. app弹框异常处理——递归方式1.1 黑名单弹框异常处理逻辑1.2 实现代码1.3 方法缺点2. app弹框异常处理——装饰器版本2.1 装饰器的优势2.2 实现代码前言: 本文为在霍格沃兹测试开发学社中学习到的一些技术写出来分享给大家,希望有志同道合的小伙伴…

计算机毕业设计之java+ssm交通信息网上查询系统

项目介绍 随着交通交通管理需求和在线交通管理渗透率的提升,中国交通管理在线市场将释放巨大潜力,交通管理系统的建设和发展成为业界广泛关注的重点,本文将对此进行分析,以期为我国交通管理电子商务的发展提供参考。交通管理业对…

石化能源行业工业互联网智能工厂解决方案

随着时代的发展,中国的工业企业逐渐进入了一个“新常态”:生产效率提升,非计划停运或检修造成的生产损失更为昂贵;高盈利的要求,需要更加关注能源使用效率;法律法规对于人员安全及环保合规要求更为严格&…

基于ffmpeg开发的多音频文件音量均衡程序

前言 audio_balance ✨ 基于ffmpeg开发的多音频文件音量均衡程序 ✨ 项目地址 GitHub:https://github.com/Ikaros-521/audio_balance gitee:https://gitee.com/ikaros-521/audio_balance 使用说明 Python:3.9 程序依赖 ffmpeg实现。请先安…

Centernet 生成高斯热图

写在前面的话 最近学校阳了,宿舍给封了,宿舍网络不好远程跑不了实验,随缘写一下对CenterNet源码的一个解读,之前写论文的那段时间留下来的工作,respect! 这个文章主要是对CenterNet中生成高斯核的部分代码…

皕杰报表之语义层

1 语义层定义 语义层——是处于数据源与报表之间的一个概念,是用户和数据库之间的一个代码翻译层,通俗的讲是将数据库中的比较凌乱、复杂的数据对象(例如:存储在table中的各个字段的记录)按预先定义好的规则&#xff…

权限管理框架Shiro renren-security权限管理结构

权限管理框架Shiro: 一直在做项目,由于是二次开发的项目,今天才发现自己连权限控制都没有搞懂。二次开发的是基于renren开源的一个项目。 链接:https://gitee.com/renrenio/renren-security 这个项目主要使用shiro权限管理框架来…

31、Java高级特性——Math类、Random类、String类、StringBuffer类、StringBuilder类

目录 一、Math类 1、Math类中的方法 1.1 圆周率:PI 1.2 绝对值:abs() 1.3 返回最小近似值:ceil() 1.4 返回最大近似值 1.5 四舍五入:round() 1.6 最大值和最小值:max()/min() 1.7 求指定次幂 :po…

Java面向对象16:接口的定义与实现

普通类:只有具体的实现 抽象类:具体的实现和规范(抽象方法)都有 接口:只有规范!自己无法写方法,专业的约束,约束和实现分离:面向接口编写(大佬把接口定义好…

vue3 响应式 API 之 ref

ref 是最常用的一个响应式 API,它可以用来定义所有类型的数据,包括 Node 节点和组件。 没错,在 Vue 2 常用的 this.$refs.xxx 来取代 document.querySelector(‘.xxx’) 获取 Node 节点的方式,也是使用这个 API 来取代。 类型声明…

[附源码]计算机毕业设计JAVA乒乓球俱乐部管理系统

[附源码]计算机毕业设计JAVA乒乓球俱乐部管理系统 项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM my…

我为什么将机器学习主力语言从Python转到Rust

我为什么将机器学习主力语言从Python转到Rust 文章目录写在前面Python的痛点猴子补丁(Monkey Patch)缺乏参数类型校验允许跨作用域访问运行缓慢太多隐含规则Rust之剑猴子补丁参数类型作用域运行速度隐含规则结论写在前面 首先要声明一下:Python依然是我最喜欢的编程…

S5PV210的启动过程

一、内存 SRAM 静态内存 特点就是容量小、价格高,优点是不需要软件初始化直接上电就能用。DRAM 动态内存 特点就是容量大、价格低,缺点就是上电后不能直接使用,需要软件初始化后才可以使用。 单片机中:内存需求量小,而…

SpringBoot SpringBoot 开发实用篇 6 监控 6.7 自定义端点

SpringBoot 【黑马程序员SpringBoot2全套视频教程,springboot零基础到项目实战(spring boot2完整版)】 SpringBoot 开发实用篇 文章目录SpringBootSpringBoot 开发实用篇6 监控6.7 自定义端点6.7.1 问题引入6.7.2 自定义端点6.7.3 小结6.7.…

20221125使用PR2023自动识别obs-studio录屏生成的MKV视频的字幕

20221125使用PR2023自动识别obs-studio录屏生成的MKV视频的字幕 2022/11/25 19:07 01 obs.png obs studio (64bit) 02 obs 设置.png 03 obs 输出.png 04 obs默认为MKV.png 05 obs改mkv为MP4.png 警告:如果文件无法完成(例如&…

供应Alkyne-PEG-Biotin,Alk-PEG-Biotin,炔烃-聚乙二醇-生物素

炔烃-聚乙二醇-生物素是一种化学PEG试剂其英文名为Alkyne-PEG-Biotin(Alk-PEG-Biotin),它所属分类为Alkyne PEG Biotin PEG。 peg试剂的分子量均可定制,有:生物素-聚乙二醇5-炔烃、生物素-PEG 20-炔烃 、Biotin-PEG 2…

【kafka】九、kafka消费者分区分配策略

消费者分区分配策略 分区分配策略 一个consumer group中有个多个topic,一个topic有多个partition,所以必然会涉及到partition的分配问题,即确定哪个partition由哪个消费者进行消费。 kafka有两种分配策略,RoundRobin和Range Ro…

JAVA实训第三天

目录 方法引用 示例 接口 类 测试类 Stream ​编辑 Stream 的操作三个步骤 创建 Stream 的 4 种方法 常见Stream接口的继承关系 Stream的中间操作 中间操作常用方法 Stream的终止操作 Stream的终止操作-collect() 示例代码演示 作业 方法引用 在Lamda新特性的支持下&…