JVM 方法调用之方法分派

2024-04-11 20:04
文章标签 java jvm 方法 调用 分派

本文主要是介绍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{@Overridepublic void say() {System.out.println("Man");}}static class Woman extends Human{@Overridepublic 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;)Vflags: (0x0009) ACC_PUBLIC, ACC_STATICCode:stack=2, locals=3, args_size=10: new           #2                  // class com/mytest/project/method/dispatch/DynamicDispatch$Man3: dup4: invokespecial #3                  // Method com/mytest/project/method/dispatch/DynamicDispatch$Man."<init>":()V7: astore_18: new           #4                  // class com/mytest/project/method/dispatch/DynamicDispatch$Woman11: dup12: invokespecial #5                  // Method com/mytest/project/method/dispatch/DynamicDispatch$Woman."<init>":()V15: astore_216: aload_117: invokevirtual #6                  // Method com/mytest/project/method/dispatch/DynamicDispatch$Human.say:()V20: aload_221: invokevirtual #6                  // Method com/mytest/project/method/dispatch/DynamicDispatch$Human.say:()V24: returnLineNumberTable:line 30: 0line 31: 8line 32: 16line 33: 20line 34: 24LocalVariableTable:Start  Length  Slot  Name   Signature0      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                           Flagsargs
}

虽然符号引用一样,但是其真正的调用版本并不相同。所以解决问题的关键,我们可以从 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;)Vson.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),使用虚方法表索引来代替元数据查找以提高性能。我们先看看上一节案例代码所对应的虚方法表结构示例,如图所示。

image-20240411143611903

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

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

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

这篇关于JVM 方法调用之方法分派的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/895050

相关文章

Nginx安全防护的多种方法

《Nginx安全防护的多种方法》在生产环境中,需要隐藏Nginx的版本号,以避免泄漏Nginx的版本,使攻击者不能针对特定版本进行攻击,下面就来介绍一下Nginx安全防护的方法,感兴趣的可以了解一下... 目录核心安全配置1.编译安装 Nginx2.隐藏版本号3.限制危险请求方法4.请求限制(CC攻击防御)

SpringBoot中六种批量更新Mysql的方式效率对比分析

《SpringBoot中六种批量更新Mysql的方式效率对比分析》文章比较了MySQL大数据量批量更新的多种方法,指出REPLACEINTO和ONDUPLICATEKEY效率最高但存在数据风险,MyB... 目录效率比较测试结构数据库初始化测试数据批量修改方案第一种 for第二种 case when第三种

python生成随机唯一id的几种实现方法

《python生成随机唯一id的几种实现方法》在Python中生成随机唯一ID有多种方法,根据不同的需求场景可以选择最适合的方案,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习... 目录方法 1:使用 UUID 模块(推荐)方法 2:使用 Secrets 模块(安全敏感场景)方法

Java docx4j高效处理Word文档的实战指南

《Javadocx4j高效处理Word文档的实战指南》对于需要在Java应用程序中生成、修改或处理Word文档的开发者来说,docx4j是一个强大而专业的选择,下面我们就来看看docx4j的具体使用... 目录引言一、环境准备与基础配置1.1 Maven依赖配置1.2 初始化测试类二、增强版文档操作示例2.

一文详解如何使用Java获取PDF页面信息

《一文详解如何使用Java获取PDF页面信息》了解PDF页面属性是我们在处理文档、内容提取、打印设置或页面重组等任务时不可或缺的一环,下面我们就来看看如何使用Java语言获取这些信息吧... 目录引言一、安装和引入PDF处理库引入依赖二、获取 PDF 页数三、获取页面尺寸(宽高)四、获取页面旋转角度五、判断

Spring Boot中的路径变量示例详解

《SpringBoot中的路径变量示例详解》SpringBoot中PathVariable通过@PathVariable注解实现URL参数与方法参数绑定,支持多参数接收、类型转换、可选参数、默认值及... 目录一. 基本用法与参数映射1.路径定义2.参数绑定&nhttp://www.chinasem.cnbs

MyBatis-Plus通用中等、大量数据分批查询和处理方法

《MyBatis-Plus通用中等、大量数据分批查询和处理方法》文章介绍MyBatis-Plus分页查询处理,通过函数式接口与Lambda表达式实现通用逻辑,方法抽象但功能强大,建议扩展分批处理及流式... 目录函数式接口获取分页数据接口数据处理接口通用逻辑工具类使用方法简单查询自定义查询方法总结函数式接口

MySQL深分页进行性能优化的常见方法

《MySQL深分页进行性能优化的常见方法》在Web应用中,分页查询是数据库操作中的常见需求,然而,在面对大型数据集时,深分页(deeppagination)却成为了性能优化的一个挑战,在本文中,我们将... 目录引言:深分页,真的只是“翻页慢”那么简单吗?一、背景介绍二、深分页的性能问题三、业务场景分析四、

JAVA中安装多个JDK的方法

《JAVA中安装多个JDK的方法》文章介绍了在Windows系统上安装多个JDK版本的方法,包括下载、安装路径修改、环境变量配置(JAVA_HOME和Path),并说明如何通过调整JAVA_HOME在... 首先去oracle官网下载好两个版本不同的jdk(需要登录Oracle账号,没有可以免费注册)下载完

Spring StateMachine实现状态机使用示例详解

《SpringStateMachine实现状态机使用示例详解》本文介绍SpringStateMachine实现状态机的步骤,包括依赖导入、枚举定义、状态转移规则配置、上下文管理及服务调用示例,重点解... 目录什么是状态机使用示例什么是状态机状态机是计算机科学中的​​核心建模工具​​,用于描述对象在其生命