JVM 方法调用之方法分派

JVM 方法调用之方法分派

文章目录

  • JVM 方法调用之方法分派
    • 1.何为分派
    • 2.静态分派
    • 3.动态分派
    • 4.单分派与多分派
    • 5.动态分派的实现

1.何为分派

在上一篇文章《方法调用之解析调用》中讲到了解析调用,而解析调用是一个静态过程,在类加载的解析阶段就确定了方法的直接引用。很明显,其他不满足解析调用的方法调用是如何确定其直接引用的呢,这就涉及到本篇文章所讲的重点概念,分派(Dispatch)。分派即可能是静态的也可能是动态的,根据分派依据的宗量数可分为单分派和多分派。所以两两组合就构成了,静态单分派、静态多分派、动态单分派及动态多分派4种情况。

方法的接受者与方法的参数统称为方法宗量。具体的宗量数如何确定,请往下看。

在往下讲解之前,需要讲明一下两个重要的概念。

Object str = new String()

以上代码中,我们把 Object 称为变量str 的“静态类型”(Static Type)或者“外观类型”(Apparent Type),后面的String 则称之为变量str的“实际类型”(Actual Type)或者“运行时类型”(Runtime Type)。因为静态类型是编译器可知的,而实际类型是在编译器不一定可知,在运行时才能真正完全确定,如下DEMO。

// 在运行前,(new Random()).nextBoolean的值是无法预知的,运行后才可得到具体值
Object obj = (new Random()).nextBoolean ? new String() : new Integer();

2.静态分派

所有依赖静态类型来决定方法调用版本的分派动作,都称为静态分派。

静态分派最典型的应用就是方法重载(Overload),静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行。另外需要注意的是,Javac编译器虽然能确定方法重载的版本,但是很多情况下,这个重载版本并不是唯一的,往往只能确定一个“相对更加合适”的版本。产生这种模糊结论的主要原因就是字面量天生的模糊性,它没有显式的静态类型,它的静态类型只能通过语义、语法规则去历届和推断。

案例代码

public class StaticDispatch {

    public static void main(String[] args) {
        say('a');
    }

    public static final void say(char c){
        System.out.println("char");
    }

    public static final void say(int c){
        System.out.println("int");
    }

    public static final void say(long c){
        System.out.println("long");
    }

    public static final void say(float c){
        System.out.println("float");
    }

    public static final void say(double c){
        System.out.println("double");
    }

    public static final void say(Character c){
        System.out.println("Character");
    }

    public static final void say(Serializable c){
        System.out.println("Serializable");
    }

    public static final void say(Object c){
        System.out.println("Object");
    }

    public static final void say(char... chars){
        System.out.println("char...");
    }

}

上述代码,由于 ‘a’ 是一个char类型的数据,所以运行结果为:

char

如果我们将say(char c)方法注释掉,那么 ‘a’ 也可以表示为字符的Unicode编码数值,即97,所以 ‘a’ 也可以表示数字97,此时 ‘a’ 发生了自动类型转换,会选择参数类型为 int 的重载版本,运行结果为:

int

如果此时再将say(int c) 方法注释掉,那么 ‘a’ 将会再发生一次自动类型转换,进一步转型为 long,输出结果如下。同理,相继注释掉后面参数类型为基本类型的重载方法,则会按照 **char > int > long > float > double **的顺序转型匹配,但是不会存在转型至byteshort类型(不安全)。

long

如果将say(long c)say(float c)say(double c)都注释掉,此时 ‘a’ 将会自动装箱为包装类型 Character,所以输出结果为:

Character

如果再将say(Character c) 注释掉,那么此时 ‘a’ 转换为包装类型 Character 后,会转换为其实现的接口,由于 SerializableCharacter 实现的一个接口,所以输出结果为:

Serializable

同理,‘a’ 转换为包装类型 Character 后,会转型为其父类,根据继承关系从下往上找,此时输出结果为:

Object

最后,变长参数的重载优先级是最低的,注释掉其他所有重载方法后,输出结果:

char...

3.动态分派

动态分派发生在运行期间,根据其实际类型确定方法调用版本。

动态分派与Java语言多态性的一个重要体现-重写(Override)关系密切。下面我们先以案例代码结合讲解。

案例代码

public class DynamicDispatch {
    
    static abstract class Human{
        public abstract void say();
    }
    
    static class Man extends Human{
        @Override
        public void say() {
            System.out.println("Man");
        }
    }

    static class Woman extends Human{
        @Override
        public void say() {
            System.out.println("Woman");
        }
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.say();
        woman.say();
    }
    
}

运行结果想必都知道:

Man
Woman

但是我们反编译字节码,可以对应的两条方法调用的符号引用(Human.say:()V)都是一样的:

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class com/mytest/project/method/dispatch/DynamicDispatch$Man
         3: dup
         4: invokespecial #3                  // Method com/mytest/project/method/dispatch/DynamicDispatch$Man."<init>":()V
         7: astore_1
         8: new           #4                  // class com/mytest/project/method/dispatch/DynamicDispatch$Woman
        11: dup
        12: invokespecial #5                  // Method com/mytest/project/method/dispatch/DynamicDispatch$Woman."<init>":()V
        15: astore_2
        16: aload_1
        17: invokevirtual #6                  // Method com/mytest/project/method/dispatch/DynamicDispatch$Human.say:()V
        20: aload_2
        21: invokevirtual #6                  // Method com/mytest/project/method/dispatch/DynamicDispatch$Human.say:()V
        24: return
      LineNumberTable:
        line 30: 0
        line 31: 8
        line 32: 16
        line 33: 20
        line 34: 24
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      25     0  args   [Ljava/lang/String;
            8      17     1   man   Lcom/mytest/project/method/dispatch/DynamicDispatch$Human;
           16       9     2 woman   Lcom/mytest/project/method/dispatch/DynamicDispatch$Human;
    MethodParameters:
      Name                           Flags
      args
}

虽然符号引用一样,但是其真正的调用版本并不相同。所以解决问题的关键,我们可以从 invokevirtual 指令的是如何实现多态查找的过程入手,根据《Java虚拟机规范》,invokevirtual 指令的运行时解析过程大致可分如下几步:

1)将当前线程的操作数栈的栈顶元素指向的对象的实际类型记做C。

2)如果在类型C 中找到与常量中的简单名称和描述符都相同的方法,则进行访问权限效验,如果通过则返回该方法的直接引用;不通过则throws an IllegalAccessError

3)否则,按照继承关系从下往上依次对C的父类进行搜索和权限效验。

4)否则,如果没有找到合适的方法(找到了抽象方法),则会throws an AbstractMethodError

4.单分派与多分派

单分派是根据一个宗量对目标方法进行选择,多分派则是根据多余一个宗量对目标方法进行选择。光从定义上可能难以理解,下面结合案例代码进行讲解。

案例代码

public class Dispatch {
  
  	static class QQ{}
  	static class _360{}
    
    static class Father{
        public void hardChoice(QQ arg){
            System.out.println("Father QQ");
        };

        public void hardChoice(_360 arg){
            System.out.println("Father _360");
        };
    }

    static class Son extends Father{
        public void hardChoice(QQ arg){
            System.out.println("Son QQ");
        };

        public void hardChoice(_360 arg){
            System.out.println("Son _360");
        };
    }

    public static void main(String[] args) {
        Father father = new Father();
        Father son = new Son();
        father.select(new QQ());  // Dispatch$Father.select:(LQQ;)V
        son.select(new _360()); // Dispatch$Father.select:(L_360;)V
    }
    
}

运行结果:

Father QQ
Son _360

在编译期,也就是静态分派过程中,选择目标方法的依据有两点:一是静态类型是 Father 还是 Son,二是方法参数是 QQ 还是 _360。很显然,这决定了最终产生的方法调用的字面量,因为是根据两个宗量进行分派的,所以在Java语言中静态分派属于多分派类型。

在运行期,也就是动态分派的过程中。实际分派起决定性作用的就是方法接受者的实际类型,因为此时的调用方法的签名已定(select:(LQQ;)V),而唯一需要进行选择的就是方法接受者,所以在Java语言里动态分派属于单分派。

5.动态分派的实现

动态分派是执行非常频繁的动作,而且动态分派的方法调用版本需要运行时在接收者类型的方法元数据中搜索合适的目标方法,因此,JVM 实现基于执行性能的考虑,真正运行时一般不会如此频繁地去反复搜索类型元数据。面对这种情况,一种基础而且常见的优化手段是为类型在方法区中建立一个虚方法表(VirtualMethod Table,也称为vtable,与此对应的,在 invokeinterface 执行时也会用到接口方法表 —— Interface Method Table,简称 itable),使用虚方法表索引来代替元数据查找以提高性能。我们先看看上一节案例代码所对应的虚方法表结构示例,如图所示。

在这里插入图片描述

虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。Son 重写了来自 Father 的全部方法,因此 Son 的方法表没有指向 Father 类型数据的箭头。但是 Son 和 Father 都没有重写来自 Object 的方法,所以它们的方法表中所有从 Object 继承来的方法都指向了 Object 的数据类型。

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

方法表是分派调用的“稳定优化”手段,虚拟机除了使用方法表之外,在条件允许的情况下,还会使用内联缓存(Inline Cache)和基于“类型继承关系分析”(Class Hierarchy Analysis,CHA)技术的守护内联(Guarded Inlining)两种非稳定的“激进优化”手段来获得更高的性能。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

4.1 返回JSON数据

1. 默认实现方式 JSON是目前主流的前后端数据传输方式&#xff0c;Spring MVC中使用消息转换器HttpMessageConverter对JSON的转换提供了很好的支持&#xff0c;在Spring Boot中更进一步&#xff0c;对相关配置做了更进一步的简化。 默认情况下&#xff0c;当开发者新创建一个S…

4.17号驱动

中断子系统 1. 中断工作原理 1.1 异常处理流程 保存现场(cpu自动完成) 保存cpsr寄存器中的值&#xff0c;到spsr_寄存器中 修改cpsr寄存器中的值 修改状态位(T位) 根据需要禁止相应的中断位(I/F) 修改对应模式位 保存函数的返回地址到lr寄存器中 修改pc指向异常向量表 …

【测试开发学习历程】python常用的模块(下)

目录 8、MySQL数据库的操作-pymysql 8.1 连接并操作数据库 9、ini文件的操作-configparser 9.1 模块-configparser 9.2 读取ini文件中的内容 9.3 获取指定建的值 10 json文件操作-json 10.1 json文件的格式或者json数据的格式 10.2 json.load/json.loads 10.3 json.du…

React 快速入门:掌握前端开发的核心技能

React 快速入门&#xff1a;掌握前端开发的核心技能 一、React 简介1.1 React 的历史1.2 React 的概念1.3 React 的特点1.4 React 的官网地址 二、开发环境搭建三、React 基础3.1 JSX3.2 组件3.3 Props3.4 State3.5 props 和 state 的区别3.6 Hook 四、React 生命周期五、添加样…

OceanBase 4.3 列存存储格式和列存索引存储格式

以 t1 表和索引为例子&#xff0c;下面两张图说明了存储层如何存储数据。 create table t1 (id1 int, id2 int, name varchar(10), salary int, primary key(id1, id2)) with column group (each column);create index idx (name) storing(salary) with column group(each co…

代码随想录算法训练营第三十七天| LeetCode 738.单调递增的数字、总结

一、LeetCode 738.单调递增的数字 题目链接/文章讲解/视频讲解&#xff1a;https://programmercarl.com/0738.%E5%8D%95%E8%B0%83%E9%80%92%E5%A2%9E%E7%9A%84%E6%95%B0%E5%AD%97.html 状态&#xff1a;已解决 1.思路 如何求得小于等于N的最大单调递增的整数&#xff1f;98&am…

【云计算】云数据中心网络(六):私网连接

云数据中心网络&#xff08;六&#xff09;&#xff1a;私网连接 1.什么是私网连接2.私网连接的组成3.私网连接的优势4.私网连接的主要应用场景 前面讲到 VPC 网络具有隔离性&#xff0c;VPC 之间无法通信。当一个 VPC 中的终端需要访问部署在另一个 VPC 中的服务时&#xff0c…

C++奇迹之旅:构造函数和析构函数

文章目录 &#x1f4dd;类的6个默认成员函数&#x1f320; 构造函数&#x1f309; 概念&#x1f309;特性&#x1f309;三种默认构造函数 &#x1f320; 特性&#x1f6a9;总结 &#x1f4dd;类的6个默认成员函数 如果一个类中什么成员都没有&#xff0c;简称为空类。 空类中真…

Redis中的订阅发布(一)

订阅发布 概述 Redis的发布与订阅功能由PUBLISH、SUBSCRIBE、PSUBSCRIBE等命令组成。通过执行SUBSCRIBER命令&#xff0c;客户端可以订阅一个或多个频道&#xff0c;从而成为这些频道的订阅者(subscribe)&#xff1a; 每当有其他客户端向被订阅的频道发送消息(message)时&…

多ip证书实现多个ip地址https加密

在互联网快速发展的现在&#xff0c;很多用户会使用由正规数字证书颁发机构颁发的数字证书&#xff0c;其中IP数字证书就是只有公网IP地址网站的用户用来维护网站安全的手段。由于域名网站比较方便记忆&#xff0c;只有公网IP地址的网站是很少的&#xff0c;相应的IP数字证书产…

基于zookeeper安装Kafka集群

操作系统&#xff1a;centOS 9 Stream&#xff0c;6台&#xff0c;基于vmware虚拟机创建 准备工作 确认系统环境&#xff1a; 确保所有服务器已安装了最新更新。安装Java Development Kit (JDK) 8或更高版本&#xff0c;因为ZooKeeper和Kafka都是基于Java开发的。例如&#x…

【配电网故障定位】基于二进制粒子群算法的配电网故障定位 33节点配电系统故障定位【Matlab代码#78】

文章目录 【获取资源请见文章第6节&#xff1a;资源获取】1. 配电网故障定位2. 二进制粒子群算法3. 算例展示4. 部分代码展示5. 仿真结果展示6. 资源获取 【获取资源请见文章第6节&#xff1a;资源获取】 1. 配电网故障定位 配电系统故障定位&#xff0c;即在配电网络发生故障…

深入理解同步与异步编程及协程管理在Python中的应用

文章目录 1. 同步与异步函数的对比1.1 同步函数1.2 异步函数1.3 对比 2. 管理多个协程与异常处理2.1 并发执行多个协程2.2 错误处理2.3 任务取消 本文将探索Python中同步与异步编程的基本概念及其区别。还会详细介绍如何使用asyncio库来有效管理协程&#xff0c;包括任务的创建…

C++动态内存管理 解剖new/delete详细讲解(operator new,operator delete)

讨厌抄我作业和不让我抄作业的人 讨厌插队和不让我插队的人 讨厌用我东西和不让我用东西的人 讨厌借我钱和不借给我钱的人 讨厌开车加塞和不让我加塞的人 讨厌内卷和打扰我内卷的人 一、C中动态内存管理 1.new和delete操作内置类型 2.new和delete操作自定义类型 二、operat…

人生的关键在于思想、精神和心情

人生的关键在于思想、精神和心情&#xff0c; 努力让自己的思想明澈&#xff0c; 让自己的精神充实而有所支撑&#xff0c; 让自己每天都有一个豁达、平和、开朗的心情&#xff0c; 这很重要。

SQLite的知名用户(二十九)

返回&#xff1a;SQLite—系列文章目录 上一篇:SQLite作为应用程序文件格式&#xff08;二十八&#xff09; 下一篇&#xff1a;SQLite—系列文章目录 SQLite被数以百万计的应用程序使用 从字面上看&#xff0c;有数十亿次部署。 SQLite 是 当今世界。 下面显示了一些…

Taro-vue微信小程序用户隐私保护

Taro-vue微信小程序用户隐私保护 一、在 微信公众平台的【设置】- 【服务内容与声明】 &#xff0c;设置用户隐私保护指引&#xff0c;添加项目需要的接口权限。 【用户隐私保护指引】提交之后&#xff0c;官方会进行审核。审核通过之后&#xff0c;对应的接口权限才会生效。 …

实现 Table 的增加和删除,不依赖后端数据回显

需求 删除前 删除后 分析 首先写一个 Table <a-card style"width:100%"><template#extra><a-button type"text" click"addSelectItem" style"margin-right: 5px">添加</a-button><a-button type&quo…

springboot源码解析(一):启动过程

springboot源码解析(一):启动过程 1、springboot的入口程序 SpringBootApplication public class StartupApplication {public static void main(String[] args) {SpringApplication.run(StartupApplication.class, args);} }当程序开始执行之后&#xff0c;会调用SpringAppli…

大话设计模式之单例模式

单例模式是一种创建型设计模式&#xff0c;它确保类只有一个实例&#xff0c;并提供一个全局访问点来访问该实例。 单例模式通常在以下情况下使用&#xff1a; 当一个类只能有一个实例&#xff0c;并且客户端需要访问该实例时。当该唯一实例需要被公开访问&#xff0c;以便在…