JVM体系-字节码执行引擎

本文详细介绍了Java虚拟机栈帧的结构,包括局部变量表和操作数栈的工作原理,以及动态连接和方法调用的过程。局部变量表用于存储方法参数和局部变量,操作数栈则在方法执行中进行数据操作。动态连接涉及到符号引用到直接引用的转化,方法调用分为解析和分派,其中动态分派基于实际类型选择方法版本。Java语言特性表现为静态多分派和动态单分派。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

运行时栈帧结构

栈帧是用于支持虚拟机进行方法调用方法执行背后的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息,每一个方法从调用开始至执行结束的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。在活动线程中,只有位于栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的。

  1. 局部变量表
    作用:局部变量表用于存放方法参数和方法内部定义 的局部变量。
    局部变量表的容量以变量槽为最小单位,都可以使用32位或更小的物理内存来存储boolean、 byte、char、short、int、float、reference或returnAddress数据类型,其中reference表示的是一个对象实例的引用。如果是64位的数据,可以分割成2个32位读写的操作。
    当一个方法被调用时,Java虚拟机会使用局部变量表来完成 即实参到形参的传递。如果执行的是实例方法(没有被static修饰的方法),那局部变量表中第0位索引的变量槽默认是用于传递方法所属对象实例的引用
    虚拟机可以根据reference引用做
    1. 从根据引用直接或间接地查找到对象在Java堆中的数据存放的起始地址或索引
    2. 根据引用直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息
    虚拟机怎么是使用局部变量表?
    通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的变量槽数量。如果访问的是32位数据类型的变量,索引N就代表了使用第N个变量槽,如果访问的是64位 数据类型的变量,则说明会同时使用第N和N+1两个变量槽。
    变量槽可以重用?怎么重用?
    变量槽可以重用,主要用来节省栈帧消耗的内存空间。当变量不再被访问时,槽的内容就能被复用,如下的代码:
    在这里插入图片描述
    此时虽然placeholder的作用域被限制在花括号以内,但并没有被回收掉。而下面这段代码placeholder被回收:
    在这里插入图片描述

placeholder能否被回收的根本原因

		局部变量表中的变量槽是否还存有 关于placeholder数组对象的引用。第一次修改中,代码虽然已经离开了placeholder的作用域,
		但在此之后,再没有发生过任何对局部变量表的读写操作,placeholder原本所占用的变量槽还没有被其他变量 所复用,所以作为GC Roots一部分的
		局部变量表仍然保持着对它的关联。

如果一个方法的代码 有一些耗时很长的操作,实际上已经不会再使用的变量,手动将其设置为null值

  1. 操作数栈
    操作数栈是一个后入先出栈。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种 字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。
    两个栈帧出现重叠的目的

     	1.节约一部分空间
     	2.方法调用时可以直接共用部分数据。
    

3.动态连接、静态解析
1. 静态解析:类加载阶段或第一次使用时,符号引用转化为直接引用。
2. 动态连接:一部分符号引用在每一次运行期间都转化为直接引用。

方法调用

任务:确定被调用方法的版本 (即调用哪一个方法),暂时还未涉及方法内部的具体运行过程。
1. 解析:将其中的一部分符号引用转化为直接引用,但这种解析成立的前提:调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。
在这里插入图片描述
只要能被invokestaticinvokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本。即Java中静态方法、私有方法、实例构造器、父类方法4种,再加上被final 修饰的方法(尽管它使用invokevirtual指令调用),这5种方法调用会在类加载的时候就可以把符号引 用解析为该方法的直接引用。
2.分派
1.静态分派:依赖静态类型来执行方法版本的分派动作
Human man =new Man();Human就是这个对象的静态类型,Man是动态类型。
典型表现:方法重载,很多情况下这个重载版本并不是“唯 一”的,往往只能确定一个“相对更合适的”版本。静态方法会在编译期确 定、在类加载期就进行解析,而静态方法显然也是可以拥有重载版本的,选择重载版本的过程也是通 过静态分派完成的。
2.动态分派:动态分派与实际类型相关。
典型表现:重写,吧常量池中符号引用解析成直接引用,根据方法接受者的实际类型来选择调用的方法版本。
怎么实现动态分派?
动态分派执行动作很繁琐,我们可以是为类型在方法 区中建立一个虚方法表,使用虚方法表索引来代替元数据查找以 提高性能。具体如下:在这里插入图片描述
虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方 法表中的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了 这个方法,子类虚方法表中的地址也会被替换为指向子类实现版本的入口地址
如果方法具有相同签名,则父类、子类的虚方法表中都应当具有一样的索引序 号。
虚方法表一般在类加载的连接阶段进行初始化。
3.Java虚拟机是如何根据实际类型来分派方法执行版本的呢?

1、找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
2、如果在类型C中找到与常量中的描述符和简单名称相符合的方法,然后进行访问权限验证,如果验证通过则返回这个方法的直接引用,查找过程结束;如果验证不通过,则抛出java.lang.IllegalAccessError异常。
3、否则未找到,就按照继承关系从下往上依次对类型C的各个父类进行第2步的搜索和验证过程。
4、如果始终没有找到合适的方法,则跑出java.lang.AbstractMethodError异常。

由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言方法重写的本质
4. 单分派和多分派
方法的接收者与方法的参数统称为方法的宗量,单分派是根据一个宗量对 目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。
在Java中,静态分派属于多分派,它需要根据 静态类型、方法参数来进行选择。动态分派是单分派,只需要根据方法接受者的实际类型进行选择就可以。
Java语言是一门静态多分派、动态单分派的语言

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值