那你真的了解方法调用吗?

方法调用是不是很熟悉?那你真的了解它吗?今天就让我们来盘一下它。

首先大家要明确一个概念,此处的方法调用并不是方法中的代码被执行,而是要确定被调用方法的版本,即最终会调用哪一个方法。

之前我们了解到,class字节码文件中的方法的调用都只是符号引用,而不是直接引用(方法在实际运行时内存布局中的入口地址),要实现两者的转化,就不得不提到解析和分派了。

解析

我们之前说过在类加载的解析阶段,会将一部分的符号引用转化为直接引用,该解析成立的前提是:方法在程序真正运行之前就已经有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。我们把这类方法的调用称为解析(Resolution)。

看到这个前提条件,有没有小伙伴联想到对象的多态性?
内心OS
没错,就是这样,在java中能满足不被重写的方法有静态方法、私有方法(不能被外部访问)、实例构造器和被final修饰的方法,因此它们都适合在类加载阶段进行解析,另外通过this或者super调用的父类方法也是在类加载阶段进行解析的。

指令集

调用不同类型的方法,字节码指令集里设置了不同的指令,在jvm里面提供了5条方法调用字节码指令:

  • invokestatic:调用静态方法,解析阶段确定唯一方法版本
  • invokespecial:实例构造器init方法、私有及父类方法,解析阶段确定唯一方法版本
  • invokevirtual:调用所有虚方法
  • invokeinterface:调用接口方法,在运行时再确定一个实现该接口的对象
  • invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

invokedynamic指令是Java7中增加的,是为实现动态类型的语言做的一种改进,但是在java7中并没有直接提供生成该指令的方法,需要借助ASM底层字节码工具来产生指令,直到java8lambda表达式的出现,该指令才有了直接的生成方式。

小知识点:静态类型语言与动态类型语言

它们的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。即静态类型语言是判断变量自身的类型信息,动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。

java类中定义的基本数据类型,在声明时就已经确定了他的具体类型了;而JS中用var来定义类型,值是什么类型就会在调用时使用什么类型。

虚方法与非虚方法

字节码指令集为invokestaticinvokespecial或者是用final修饰的invokevirtual的方法的话,都可以在解析阶段中确定唯一的调用版本,符合这个条件的就是我们上边提到的五类方法。它们在类加载的时候就会把符号引用解析为该方法的直接引用,这些方法可以称为非虚方法。与之相反,不是非虚方法的方法是虚方法
废话还用你说?

分派

如果我们在编译期间没有将方法的符号引用转化为直接引用,而是在运行期间根据方法的实际类型绑定相关的方法,我们把这种方法的调用称为分派。其中分派又分为静态分派和动态分派。

静态分派

不知道你对重载了解多少?为了解释静态分派,我们先来个重载的小测试:

public class StaticDispatch {static abstract class Human {}static class Man extends Human {}static class Woman extends Human {}public void sayHello(Human guy) {System.out.println("hello,guy!");}public void sayHello(Man guy) {System.out.println("hello,gentleman!");}public void sayHello(Woman guy) {System.out.println("hello,lady!");}public static void main(String[] args) {Human man = new Man();Human woman = new Woman();StaticDispatch sr = new StaticDispatch();sr.sayHello(man);sr.sayHello(woman);}
}

请考虑一下输出结果,沉默两分钟。答案是

hello,guy!
hello,guy!

你答对了嘛?首先我们来了解两个概念:静态类型和实际类型。拿Human man = new Man();来说Human称为变量的静态类型,而Man我们称为变量的实际类型,区别如下:

  1. 静态类型的变化仅仅在使用时才发生,变量本身的静态类型是不会被改变,并且最终静态类型在编译期是可知的。
  2. 实际类型的变化是在运行期才知道,编译器在编译程序时并不知道一个对象的具体类型是什么。

此处之所以执行的是Human类型的方法,是因为编译器在重载时,会通过参数的静态类型来作为判定执行方法的依据,而不是使用实际类型

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用就是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的,而是由编译器来完成。

动态分派

了解了重载之后再来了解下重写?案例走起:

public class DynamicDispatch {static abstract class Human{protected abstract void sayHello();}static class Man extends Human{@Overrideprotected void sayHello() {System.out.println("man say hello!");}}static class Woman extends Human{@Overrideprotected void sayHello() {System.out.println("woman say hello!");}}public static void main(String[] args) {Human man = new Man();Human woman = new Woman();man.sayHello();woman.sayHello();man = new Woman();man.sayHello();}}

请考虑一下输出结果,继续沉默两分钟。答案是:

man say hello!
woman say hello!
woman say hello!

这次相信大家的结果都对了吧?我们先来补充一个知识点:

父类引用指向子类时,如果执行的父类方法在子类中未被重写,则调用自身的方法;如果被子类重写了,则调用子类的方法。如果要使用子类特有的属性和方法,需要向下转型。

根据这个结论我们反向推理一下:manwomen是静态类型相同的变量,它们在调用相同的方法sayHello()时返回了不同的结果,并且在变量man的两次调用中执行了不同的方法。导致这个现象的原因很明显,是这两个变量的实际类型不同,Java虚拟机是如何根据实际类型来分派方法执行版本的呢?我们看下字节码文件:
字节码文件

man.sayHello();
woman.sayHello();

我们关注的是以上两行代码,他们对应的分别是17和21行的字节码指令。单从字节码指令角度来看,它俩的指令invokevirtual和常量$Human.sayHello:()V是完全一样的,但是执行的结果确是不同的,所以我们得研究下invokevirtual指令了,操作流程如下:
请开始你的表演

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

由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令并不是把常量池中方法的符号引用解析到直接引用上就结束了,还会根据接收者的实际类型来选择方法版本(案例中的实际类型为ManWoman),这个过程就是Java语言中方法重写的本质

我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。

单分派与多分派

方法的接收者与方法的参数统称为方法的宗量,这个定义最早应该来源于《Java与模式》一书。根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。

举例说明

public class Dispatch{static class QQ{}static class_360{}public static class Father{public void hardChoice(QQ arg){System.out.println("father choose qq");}public void hardChoice(_360 arg){System.out.println("father choose 360");}}public static class Son extends Father{public void hardChoice(QQ arg){System.out.println("son choose qq");}public void hardChoice(_360 arg){System.out.println("son choose 360");}}public static void main(String[]args){Father father=new Father();Father son=new Son();father.hardChoice(new_360());son.hardChoice(new QQ());}
}

请考虑一下输出结果,继续沉默两分钟。答案是:

father choose 360
son choose qq

我们来看看编译阶段编译器的选择过程,也就是静态分派的过程。这时选择目标方法的依据有两点:一是静态类型是Father还是Son,二是方法参数是QQ还是360。这次选择结果的最终产物是产生了两条invokevirtual指令,两条指令的参数分别为常量池中指向Father.hardChoice(360)Father.hardChoice(QQ)方法的符号引用。因为是根据两个宗量进行选择,所以Java语言的静态分派属于多分派类型。

再看看运行阶段虚拟机的选择,也就是动态分派的过程。在执行“son.hardChoice(new QQ())”这句代码时,更准确地说,是在执行这句代码所对应的invokevirtual指令时,由于编译期已经决定目标方法的签名必须为hardChoice(QQ),虚拟机此时不会关心传递过来的参数“QQ”到底是“腾讯QQ”还是“奇瑞QQ”,因为这时参数的静态类型、实际类型都对方法的选择不会构成任何影响,唯一可以影响虚拟机选择的因素只有此方法的接受者的实际类型是Father还是Son。因为只有一个宗量作为选择依据,所以Java语言的动态分派属于单分派类型。

虚方法表

在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就很可能影响到执行效率。因此,为了提高性能,jvm采用在类的方法区建立一个虚方法表(Vritual Method Table,也称为vtable,与此对应的,在invokeinterface执行时也会用到接口方法表——Inteface Method Table,简称itable)来实现,使用虚方法表索引来代替元数据查找以提高性能。
虚方法表

每一个类中都有一个虚方法表,表中存放着各种方法的实际入口:

  • 如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。
  • 如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。

Son重写了来自Father的全部方法,因此Son的方法表没有指向Father类型数据的箭头。但是SonFather都没有重写来自Object的方法,所以它们的方法表中所有从Object继承来的方法都指向了Object的数据类型。

为了程序实现上的方便,具有相同签名的方法,在父类、子类的虚方法表中都应当具有一样的索引序号,这样当类型变换时,仅需要变更查找的方法表,就可以从不同的虚方法表中按索引转换出所需的入口地址。方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕。

绑定机制

解析调用一定是个静态的过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期再去完成。分派(Dispatch)调用则可能是静态的也可能是动态的。因此我们把 解析 和 静态分派 这俩在编译期间就确定了被调用的方法,且在运行期间不变的调用称之为静态链接,而在运行期才确定下来调用方法的称之为动态链接。

我们把在静态链接过程中的转换成为早期绑定,将动态链接过程中的转换称之为晚期绑定。

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

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

相关文章

Android C++系列:Linux Socket编程(三)CS模型示例

1. TCP通信 下图是基于TCP协议的客户端/服务器程序的一般流程: 服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于 监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服 务器应答,服务器应答一个SYN-ACK段,客户…

Blazor SPA 的本质是什么以及服务器端渲染如何与 Blazor 的新 Web 应用程序配合使用

Blazor 通常被称为单页应用程序 (SPA) 框架。当我第一次开始使用 Blazor 时,我对 SPA 的含义、组件如何为 SPA 架构做出贡献以及所有这些如何与交互性联系在一起感到困惑。 今天,我将解答大家可能关心的三个问题: 什么是 SPA?了…

【高中数学/基本不等式】当x是正实数时,求函数f(x)=x/(1+x^2)的最大值?

【问题】 当x是正实数时&#xff0c;求函数f(x)x/(1x^2)的最大值&#xff1f; 【解答】 解&#xff1a; f(x)x/(1x^2)1/(x1/x))<1/2倍根号下(x*1/x)1/2 所以函数在[0,∞)的区域最大值为0.5 【函数图像】 f(x)x/(1x^2)是奇函数&#xff0c;没有断点&#xff0c;是可以…

文心一言最常用的20条指令及指令说明,含增强指令

下面是20条文心一言的指令及其说明&#xff0c;每条指令尽量简洁明了&#xff0c;以便在有限的字数内提供尽可能多的信息。以下是这些指令及其说明&#xff1a; 1. 查询天气 指令&#xff1a;今天北京的天气怎么样&#xff1f;说明&#xff1a;此指令用于查询特定城市&#xf…

HarmonyOS Next系列之Echarts图表组件(折线图、柱状图、饼图等)实现(八)

系列文章目录 HarmonyOS Next 系列之省市区弹窗选择器实现&#xff08;一&#xff09; HarmonyOS Next 系列之验证码输入组件实现&#xff08;二&#xff09; HarmonyOS Next 系列之底部标签栏TabBar实现&#xff08;三&#xff09; HarmonyOS Next 系列之HTTP请求封装和Token…

Redis高级篇之最佳实践

Redis高级篇之最佳实践 今日内容 Redis键值设计批处理优化服务端优化集群最佳实践 1、Redis键值设计 1.1、优雅的key结构 Redis的Key虽然可以自定义&#xff0c;但最好遵循下面的几个最佳实践约定&#xff1a; 遵循基本格式&#xff1a;[业务名称]:[数据名]:[id]长度不超过…

PCIe 规范核心知识线介绍

0&#xff0c;总体Topology x86 处理器系统中 PCIe的拓扑结构&#xff1a; PCIe Switch的总体结构 1&#xff0c;PCIe 枚举 BIOS 负责枚举与分派配置设备的 BusID[7:0] : DeviceID[4:0] : FunctionID[2:0]; cpu先识别 Host-PCI-Bridge&#xff0c;其下是Bus0&#xff1b; 在…

树莓派学习笔记18:IIC驱动_PCA9685(16路舵机驱动模块)误发

今日继续学习树莓派4B 4G:(Raspberry Pi,简称RPi或RasPi) 本人所用树莓派4B 装载的系统与版本如下: 版本可用命令 (lsb_release -a) 查询: ​ Python 版本3.7.3: ​ IIC驱动_PCA9685(16路舵机驱动模块) 文章提供测试代码讲解,整体代码贴出、测试效果图 目录 开启树莓…

什么是 HTTP POST 请求?初学者指南与示范

在现代网络开发领域&#xff0c;理解并应用 HTTP 请求 方法是基本的要求&#xff0c;其中 "POST" 方法扮演着关键角色。 理解 POST 方法 POST 方法属于 HTTP 协议的一部分&#xff0c;主旨在于向服务器发送数据以执行资源的创建或更新。它与 GET 方法区分开来&…

尚硅谷 一 JS简介

一 JS简介 1.1 JS起源 Javascript是一种由Netscape(网景)的LiveScript发展而来的原型化继承的面向对象的动态类型的区分大小写的客户端脚本语言&#xff0c;主要目的是为了解决服务器端语言&#xff0c;遗留的速度问题&#xff0c;为客户提供更流畅的浏览效果。当时服务端需要…

vue3+vue-router+vite 实现动态路由

文章中出现的代码是演示版本&#xff0c;仅供参考&#xff0c;实际的业务需求会更加复杂 什么是动态路由 什么场景会用到动态路由 举一个最常见的例子&#xff0c;比如说我们要开发一个后台管理系统&#xff0c;一般来说后台管理系统都会分角色登录&#xff0c;这个时候也就涉…

第4章 课程发布:模块需求分析,课程预览(模板引擎 静态页面),课程审核,课程发布(分布式事务,页面静态化:熔断降级),课程搜索(es索引)

1 模块需求分析 1.1 模块介绍 课程信息编辑完毕即可发布课程&#xff0c;发布课程相当于一个确认操作&#xff0c;课程发布后学习者在网站可以搜索到课程&#xff0c;然后查看课程的详细信息&#xff0c;进一步选课、支付、在线学习。 下边是课程编辑与发布的整体流程&#…

一.2.(1)双极型晶体三极管的结构、工作原理、特性曲线及主要参数

1.双极型晶体三极管的结构 学会区分P管和N管&#xff0c;会绘制符号 2.工作原理 无论是PNP 还是NPN&#xff0c;本质上放大时&#xff0c;都是发射结正偏&#xff0c;集电极反偏。&#xff08;可以简单理解为pn为二极管&#xff0c;每个三极管都有两个二极管&#xff09; 其中电…

前端面试题22(js中sort常见的用法)

JavaScript 的 sort() 方法是数组的一个非常强大的功能&#xff0c;用于对数组的元素进行排序。这个方法直接修改原数组&#xff0c;并返回排序后的数组。sort() 的默认行为是将数组元素转换为字符串&#xff0c;然后按照字符串的 Unicode 字典顺序进行排序。这意味着如果你试图…

Linux内存管理--系列文章柒——硬件架构

一、引子 之前文章讲解的是系统的虚拟内存&#xff0c;本章讲述这些硬件的架构和系统怎样统一管理这些硬件的。 二、物理内存模型 物理内存模型描述了计算机系统中的物理内存如何由操作系统组织和管理。它定义了物理内存如何划分为单元&#xff0c;如何寻址这些单元以及如何…

powershell美化工具Oh My Posh安装教程

1. 安装Oh My Posh 进入Oh My Posh官网&#xff0c;可根据不同平台进行下载 windows下可以直接在微软商店下载 2. 安装Nerd Fonts字体 进入Nerd Fonts官网&#xff0c;选择自己喜欢的字体下载解压后&#xff0c;全选所有文件&#xff0c;右键选择安装即可&#xff08;忽略LICEN…

ARM_Linux驱动开发——字符设备驱动开发(上)

目录 一、Linux驱动开发思维 二、Linux驱动开发分类 三、“ ARM_Linux驱动开发——字符设备驱动开发 ” 字符设备驱动简介 前言 在分享Linux驱动开发之前&#xff0c;我想带大家首先回顾一下裸机驱动开发和Linux驱动开发的区别。 1、运行环境和操作系统&#xff1a; 裸机驱…

Apache Seata 高可用部署实践

本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 Apache Seata 高可用部署实践 Seata 高可用部署实践 使用配置中心和数据库来实现 Seata 的高…

使用Spring Boot和自定义缓存注解优化应用性能

在现代应用开发中&#xff0c;缓存是提高系统性能和响应速度的关键技术之一。Spring Boot提供了强大的缓存支持&#xff0c;但有时我们需要更灵活的缓存控制。本文将介绍如何使用Spring Boot和自定义缓存注解来优化应用性能。 1. 为什么需要自定义缓存注解&#xff1f; Sprin…

AI算力革命:GPU租赁与算力市场的未来趋势

在数字化和智能化的时代背景下&#xff0c;人工智能&#xff08;AI&#xff09;的快速发展已成为全球科技领域的焦点。AI算力作为支撑其发展的关键因素&#xff0c;正以前所未有的速度改变着我们的生活和工作方式。其中&#xff0c;GPU租赁模式的兴起&#xff0c;不仅解决了AI算…