JVM如何确定方法调用

news2024/10/5 21:35:30

方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定调用哪一个方法,不涉及方法内部的具体运行过程。在程序运行时,进行方法调用是最普遍、最频繁的操作,但Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于之前所说的直接引用)。这个特性给Java带来了更强大的动态扩展能力,但也使得Java方法的调用过程变得相对复杂起来,需要在类加载期间甚至到运行期间才能确定目标方法的直接引用。好比多态

了解JVM如何确定方法的调用能够帮助我们去更加深入认识Java多态,在了解JVM如何确定方法调用之前,我们需要先声明两个概念名词:解析分派
声明:以下的内容适用于JDK8及更小版本,高版本并未实验所以不提供高版本的参考依据

1.解析

如果一个方法在编译器就可以确定调用的哪个方法→也就是调用目标在程序代码写好、编译器进行编译时就必须确定下来,这类方法的调用称为解析。这类方法再类加载的时候就会把方法的符号引用转换为方法的直接引用地址。

这类方法JVM提供了两条方法调用字节码指令:

  1. invokestatic:调用静态方法
  2. invokesepecial:调用特殊方法(调用实例构造方法<init>,私有方法,和父类方法)
    静态方法:因为静态方法更多被我们称为类方法,它是属于类的,所以一旦通过类去调用其静态方法是能够确定一个唯一的方法的。

特殊方法:

  • 构造方法是属于一个类的,且可以通过以下方法调用:this()子类的super()new Construct(),不管是哪种调用,都是能够确定到某一个类的构造方法的。
  • 私有方法是不能被子类重写的同时不能被外部调用,因为子类访问不了被private修饰的方法,所以访问不了也就无法重写(即使强制重写也会被编译器提示)。所以一个私有方法的调用是能够确定一个唯一方法的。
  • 父类方法调用能确定唯一一个方法是因为一个类只能继承一个父类,所以如果调用super.xxx()那么就能确定调用一个父类的xxx方法

除了上面的两个方法调用字节码外还需要注意的是final方法属于invokevirtual方法,但是又因为它无法被覆盖→实现多态,或者说对于多态的选择的结果是唯一的,所以final并不是虚方法,且可以再编译器就可以将符号引用转换为直接引用。

2.分派

除了解析这个概念,还有一个概念是分派,我们知道解析就是再编译器就确定方法的引用,而分派则是即可能是静态也可能是动态,它是根据宗量数可分为单分派和多分派。每种分派有可以分为静态和动态,所以一共有4种。分派将会揭露多态性的特征的一些最基本的体现,如重载,重写。

  • 宗量:方法的接收者与方法的参数的统称。
  • 宗量数:一次方法调用中未知具体引用的数量。

静态分派

静态分派(Static Dispatch)是指在编译时确定方法或函数调用的实际执行版本。在静态分派中,编译器根据引用对象的静态类型(即声明时的类型,而不考虑对象的运行时类型)来确定调用哪个方法或函数

  • 静态分派的一个典型应用场景是方法重载(Overloading)。方法重载中,编译器会根据方法参数的静态类型来选择调用哪个版本的方法。虽然在编译时就确定了调用的方法,但实际执行的是符合参数动态类型的版本
  • 编译器在选择静态分派目标时会根据静态类型的优先级来决定重载的,在一些情况下编译器虽然能确定出方法的重载版本,但是当重载版本不唯一时,往往只能确定一个更加合适的版本。一个字面量的的静态类型只能通过语言上的规则去理解和推断。

方法重载案例

如下我们通过重载来演示静态分派选择方法调用。我们定义一个父类Human,同时两个类同时继承这个Human,当我们调用sayHello方法时,无论实际类型是Man还是Woman,最终调用的都是以Human类型为参数的sayHello方法。所以静态分派是能够在编译期间就能确定方法的调用的。

public class StaticDispatch {

    static abstract class  Human {}

    static class Man extends Human {}

    static class Woman extends Human {}

    public void sayHello(Human human) {
        System.out.println("hello,human");
    }

    public void sayHello(Man man) {
        System.out.println("hello,man");
    }

    public void sayHello(Woman woman) {
        System.out.println("hello,woman");
    }


    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch sc = new StaticDispatch();
        sc.sayHello(man);        // 最终输出 hello,human
        sc.sayHello(woman);        // 最终输出 hello,human
    }
}

更加合适版本推断

如下的代码就是当一个输入在选择重载方法时有多种选择时,会如何选择更加版本的案例。

  • 下面的代码执行时会选择char类型的重载版本,当注释了char类型的方法,会执行int类型,…
  • 主要遵循的优先级就是char->int->long->Character->Serializable->Object->char…
    • 可以看出可变参的优先级最低,需要注意的是,Character的实现接口还有一个Comparable,所以当Serializable和Comparable两个类型的重载都出现时,编译器会拒绝编译,因为它猜不出来你想重载哪一个方法。除非你在调用方法时指定类型->sayHello((Serializable)‘a’);

public class Overload {
    
    public static void sayHello(Object arg) {
        System.out.println("hello,Object");
    }


    public static void sayHello(int arg) {
        System.out.println("hello,int");
    }

    public static void sayHello(long arg) {
        System.out.println("hello,long");
    }

    public static void sayHello(Character arg) {
        System.out.println("hello,Character");
    }

    public static void sayHello(char arg) {
        System.out.println("hello,char");
    }

    public static void sayHello(char... arg) {
        System.out.println("hello,char ...");
    }

    public static void sayHello(Serializable arg) {
        System.out.println("hello,Serializable");
    }
    
    public static void main(String[] args) {
        sayHello('a');
    }
    
}

动态分派

动态分派(Dynamic Dispatch)是指在运行时根据对象的实际类型来确定调用哪个方法或函数的版本。在动态分派中,方法的实际执行版本是根据对象的运行时类型来决定的,而不是根据编译时类型。动态分派通常与面向对象编程中的多态性相关联。

动态分派的工作原理是:当调用一个方法时,虚拟机会首先检查对象的实际类型,然后根据该类型确定调用哪个方法版本。这种机制使得程序能够根据对象的实际状态动态地选择执行的方法,从而实现多态性。

方法重写

动态分派的一个典型应用场景是方法重写(Overriding)。在方法重写中,子类可以重写父类的方法,当调用该方法时,实际执行的是子类中的版本,而不是父类中的版本。

接下来的案例将会展示动态分派在方法重写中的案例,当一个Human父类,被ManWoman继承后,被重写sayHello方法,当我们分别new ManWoman对象时,去调用sayHello方法,实际上调用的就是对象的实际类型的重写后的sayHello方法

public class DynamicDispatch {
    
    
    static abstract class Human {
        protected abstract void sayHello();
    }
    
    static class Man extends Human {
        @Override
        protected void sayHello() {
            System.out.println("hello man");
        }
    }
    
    static class Woman extends Human {
        @Override
        protected void sayHello() {
            System.out.println("hello woman");
        }
    }


    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello(); // out: hello man
        woman.sayHello(); // out: hello woman
        man = new Woman();
        man.sayHello(); // out: hello woman
    }
    
}
JVM如何知道要调用的是哪个类型的方法

我们可以通过字节码去了解JVM是如何知道我们要调用的方法的。

new #2 <xxx/DynamicDispatch$Man>
dup
invokespecial #3 <xxx/DynamicDispatch$Man.<init> : ()V>
astore_0
new #4 <xxx/DynamicDispatch$Woman>
dup
invokespecial #5 <xxx/DynamicDispatch$Woman.<init> : ()V>
astore_1
aload_0
invokevirtual #6 <xxx/DynamicDispatch$Human.sayHello : ()V>
aload_1
invokevirtual #6 <xxx/DynamicDispatch$Human.sayHello : ()V>
new #4 <xxx/DynamicDispatch$Woman>
dup
invokespecial #5 <xxx/DynamicDispatch$Woman.<init> : ()V>
astore_0
aload_0
invokevirtual #6 <xxx/DynamicDispatch$Human.sayHello : ()V>
return

实际调用方法的指令为invokevirtual,造成两个对象man和woman的调用结果不一样的原因就是invokevirtual的多态查找过程:

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

可以从第一步中看出,因为两个对象的实际类型不一样,从而找到了两个方法的重写的直接引用返回。我们将这个运行期根据实际类型确定方法版本的分派称为动态分派

单分派与多分派

根据分派基于多少种宗量,可以将分派分为单分派和多分派。宗量的概念我们在前面提到过。

  • 单分派:根据一个宗量对目标方法进行选择。
  • 多分派:根据多于一个的宗量对目标方法进行选择。

通过如下代码可以确定:

  • 编译阶段编译器的选择过程,即静态分派的过程。这时候选择目标方法的依据有两点:一是静态类型是Father还是Son,二是方法参数是Man还是Woman。这次选择结果的最终产物是产生了两条指令,参数分别为常量池中指向Father.hardChoice(Man)Father.hardChoice(Woman)方法的符号引用。因为是根据两个宗量进行选择,所以Java语言的静态分派属于多分派类型
  • 运行阶段虚拟机的选择,即动态分派的过程。在执行son.hardChoice(new Man())这句代码时,由于编译期已经决定目标方法的签名必须为hardChoice(Man),因为在传递参数的时候就已经确定了一个重载方法(因为参数的静态类型Man),唯一可以影响虚拟机选择的因素只有此方法的接收者的实际类型是Father还是Son。因为只有一个宗量作为选择依据,所以Java语言的动态分派属于单分派类型
public class Dispatch {
    
    static class Man{}
    
    static class Woman{}
    
    public static class Father {
        public void hardChoice(Man arg) {
            System.out.println("father choose man");
        }
        
        public void hardChoice(Woman arg) {
            System.out.println("father choose woman");
        }
    }
    
    public static class Son extends Father{
        @Override
        public void hardChoice(Man arg) {
            System.out.println("son choose man");
        }
        @Override
        public void hardChoice(Woman arg) {
            System.out.println("son choose woman");
        }
    }

    public static void main(String[] args) {
        Father father = new Father();
        Father son = new Son();
        father.hardChoice(new Woman());// out:father choose woman

        son.hardChoice(new Man());// out:son choose man
    }
    
}

虚拟机动态分派的实现

动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法
所以虚拟机在实际现实中基于性能考虑,大部分实现都不会真的进行如此频繁的搜索。
最常用的"稳定优化"手段就是为类在方法区建立一个虚方法表(vtable->Virtual Method Table)。与此对应在invokeinterface执行时也会用到接口的方法表–Interface Method Table,检查itable,使用虚方法表索引来替代元数据查找以提高性能。
在这里插入图片描述

  • 虚方法表存放着各个方法的实际入口地址,如果某个方法在子类中没有被重写,那么子类的虚方法表中的地址入口会和父类的地址入口一致。若重写了,则子类指向重写的现实版本的入口地址。
    • 如图Son重写了的两个方法指向了Son的类型数据,而Father类指向了Father的类型数据,并且Son和Father都继承自Object但并没有重写其方法,所以方法都指向了Object类型数据。
  • 方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕。
  • JVM除了使用方法表这种"稳定优化"的手段外,还会使用内联缓存和基于类型继承关系分析(CHA)技术的守护内联两种稳定的"激化优化"来获取更高的性能。

内联缓存和机遇类型继承关系分析技术守护内联

由于本篇主要是介绍分派的实现原理,并不展开讲内联缓存和基于类型继承关系分析(CHA)技术的守护内联,所以只做解释

内联缓存(Inline Cache)
  • 这是一种用于提高方法调用性能的技术。在Java中,方法调用通常是通过虚方法表(Virtual Method Table,VMT)实现的,即在运行时根据对象的实际类型来确定调用哪个方法。然而,这种动态的方法调用会带来一定的性能开销。
  • 为了提高方法调用的性能,JVM会对频繁调用的方法做内联优化。内联是将被调用的方法的字节码直接嵌入到调用处,避免了方法调用的开销。而内联缓存则用于缓存已经内联的方法的调用位置和目标方法,以便在下次调用时能够快速定位并直接执行目标方法,而不需要再次查找虚方法表。

内联缓存的优点在于能够减少方法调用的开销,提高程序的性能。然而,它也存在一定的限制,例如内联缓存的大小和命中率会影响性能,过大的缓存可能会导致内存占用过多。

基于类型继承关系分析(CHA)技术的守护内联(Guarded Inlining)
  • 这是一种静态分析技术,用于分析程序中类之间的继承关系。通过CHA分析,JVM可以在编译时确定方法调用的可能的接收者类型,并根据这些信息进行优化。
  • 守护内联是基于CHA分析的一种优化技术,它通过判断方法调用的接收者类型是否唯一,以及方法是否被重写等信息,来确定是否进行内联优化。如果接收者类型唯一,并且方法未被重写,则可以安全地进行内联优化。否则,可能需要保留动态调度的方式来处理。

守护内联的优点在于可以根据静态分析的结果来进行更加精确的内联决策,提高内联的命中率和性能。然而,它也需要进行更复杂的分析和判断,可能会增加编译器的复杂度和开销。

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

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

相关文章

破解动态网页:如何用JavaScript获取自动消失的联想词

前几天在做数据分析时&#xff0c;我尝试获取某网站上输入搜索词后的联想词&#xff0c;输入搜索词后会弹出一个显示联想词的框。有趣的是&#xff0c;当我尝试通过按F12定位这个弹框在HTML中的位置时&#xff0c;输入框失去焦点后&#xff0c;联想词弹框就自动消失了。我观察到…

UnityAPI学习之Animator的基本使用

动画与动画控制器 示例1&#xff1a; 创建Animator对动画控制器进行统一管理&#xff0c;在Gris中创建Animator组件&#xff0c;并对其中的Controller属性进行赋值 在进行动画创作前&#xff0c;需先将图片的Texture Type属性改为Sprite(2D and UI) 再将一系列图片拖入Gris物…

nss刷题(4)

1、[SWPUCTF 2021 新生赛]easyrce <?php error_reporting(0); highlight_file(__FILE__); if(isset($_GET[url])) { eval($_GET[url]); } ?> if(isset($_GET[url])) isset函数用来检测url变量是否存在&#xff1b;$_GET函数获取变量数据 eval($_GET[url]); eval函数用…

基于Java+Swing+mysql幼儿园信息管理系统V2

博主介绍&#xff1a; 大家好&#xff0c;本人精通Java、Python、C#、C、C编程语言&#xff0c;同时也熟练掌握微信小程序、Php和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我有丰富的成品Java、Python、C#毕设项目经验&#xff0c;能够为学生提供各类…

和鲸101领航北中医:助力健康医疗AI实验室建设,培养交叉数据人才

2024 年 3 月开学季&#xff0c;北京中医药大学&#xff08;简称“北中医”&#xff09;的健康医疗人工智能实验室迎来了正式投入使用后的第一堂课。除了配备全新的桌椅和尖端的硬件服务器外&#xff0c;实验室还引入了先进的人工智能实训平台&#xff0c;为大数据管理与应用专…

Linux1(介绍与基本命令)

目录 一、初始Linux 1. Linux的起源 2. Linux是什么&#xff1f; 3. Linux内核版本 4. Linux的应用 5. 终端 6. Shell 7. Linux目录结构 二、基本命令 1. 基本的命令格式 2. shutdown 关机命令 3. pwd 当前工作目录 4. ls 查看目录内容 5. cd 改变工作目录 …

【制作100个unity游戏之27】使用unity复刻经典游戏《植物大战僵尸》,制作属于自己的植物大战僵尸随机版和杂交版10(附带项目源码)

最终效果 系列导航 文章目录 最终效果系列导航前言使用DoTween优化阳光生成和拾取效果拾取阳光优化生成阳光优化 场景加载进度条新增加载场景Loading&#xff0c;绘制开始界面绘制菜单界面滑动滚轮一直滚动 场景加载源码结束语 前言 本节主要实现使用DoTween优化阳光生成和拾取…

Linux运维实用小脚本,登录即自动显示系统信息

systeminfo.sh #!/bin/bash # systeminfo.sh # by 运维朱工 # site&#xff1a;bash.lutixia.cn ##################################### 获取IP地址和主机名 IP_ADDR$(hostname -I | cut -d -f1) HOSTNAME$(hostname)# CPU负载信息&#xff1a; cpu_load() {echo -e "\…

JAVA基础--MAVEN

MAVEN的认识 什么是MAVEN Maven是一个项目构建及管理工具&#xff0c;开发团队几乎不用花多少时间就能够自动完成工程的基础构建配置&#xff0c; Maven 使用了一个标准的目录结构在不同开发工具中也能实现项目结构的统一。 统一项目结构 Maven提供了清理&#xff0c;编译&a…

【二进制部署k8s-1.29.4】十三、metrics-server的安装部署

文章目录 简介 一.metrics-server的安装 简介 本章节主要讲解metrics-server的安装&#xff0c;metrics-server主要是用于采集k8s中节点和pod的内存和cpu指标&#xff0c;在观察几点和pod的实时资源使用情况还是比较有用的&#xff0c;如果需要记录历史信息&#xff0c;建议采用…

Java到AI大模型,我为什么选择的后者

我为什么从Java转到AI大模型 在编程的海洋里&#xff0c;Java一直是我信赖的“小船”&#xff0c;载着我航行在代码的世界中。然而&#xff0c;随着行业的不断发展和变化&#xff0c;我开始感受到了一丝的迷茫和不安。我开始担心&#xff0c;随着技术的不断更新&#xff0c;Ja…

材料科学基础:期末计算题(第6章)结晶驱动力与过冷度

材料科学基础&#xff1a;计算题&#xff08;第6章&#xff09; 结晶驱动力与过冷度 ∆ G < 0 ; G H − T S ∆G<0; GH-TS ∆G<0;GH−TS d G d T d H d T − S − T d S d T \frac{dG}{dT}\frac{dH}{dT}-S-T\frac{dS}{dT} dTdG​dTdH​−S−TdTdS​ d G d T d H d …

B站画质补完计划(3):智能修复让宝藏视频重焕新生

1 老片存在什么画质问题&#xff1f; B站作为一个拥有浓厚人文属性的平台社区&#xff0c;聚集了诸如《雍正王朝》、《三国演义》等经典影视剧集&#xff0c;同时也吸引了大量用户欣赏、品鉴这些人文经典 。但美中不足的是&#xff0c;由于拍摄年代久远、拍摄设备落后、数据多次…

一次会见苹果App Review专家的在线研讨会

本篇我们来聊聊一次会见苹果App Review专家的见闻&#xff0c;希望能够借助本次会见的内容纪要分享&#xff0c;给广大出海的iOS开发者提供一些有价值的资讯信息&#xff0c;帮助大家都能够轻松应对App的每一次审核。 近期&#xff0c;小编收到了来自苹果设计开发加速器的邀请…

园区无线网新架构:无CAPWAP的集中式转发

1、从经典的APAC组网说起 谈及园区无线网&#xff0c;大家脑子里不免会蹦出同一个关键词。 没错&#xff0c;市面上常见的中大型企业/园区的无线网络组网方案&#xff0c;大多都是基于集中式网关转发的”APAC”模式。 顾名思义&#xff0c;该架构包括 AP 和AC两个关键角色。 …

力扣每日一题 6/12 + 随机一题

博客主页&#xff1a;誓则盟约系列专栏&#xff1a;IT竞赛 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ 2806.取整够买后的账户余额【简单】 题目&#xff1a; 一开始&#xff0c;你…

脾虚,人就废了一半!脾虚分3种,分清是哪一种,才能对症补脾!

入夏养什么&#xff1f;除心之外&#xff0c;还要多养养脾胃&#xff01;因为夏季暑热潮湿&#xff0c;加上天气变热后&#xff0c;大家喜欢吃冰的食物&#xff01;“喜燥恶湿”的脾胃在夏季就很容易受伤&#xff0c;导致脾虚&#xff01; 中医认为&#xff0c;脾主运化&#x…

ArcGIS Pro 3.0加载在线高德地图

1、打开ArcGIS Online官网&#xff0c;登录自己的账号&#xff0c;登录后效果如下图所示 官网地址&#xff1a;https://www.arcgis.com/home/webmap/viewer.html 2、点击Add&#xff0c;选择Add Layer from Web&#xff0c;如下图所示 3、在显示的Add Layer from Web页面内&am…

GA/T 1400 (非标)视图库网关

GA/T 1400 &#xff08;非标&#xff09;视图库网关 应用概述&#xff1a; GAT1400视图库网关产品是公司“分布式综合安防管理平台”下的子系统 针对以下遇到应用场景定制开发、优化后形成的网关产品&#xff0c;具备兼容性高、可扩展、可功能定制、可OEM等优点。 视图库网关…

python中魔术方法__str__与__repr__的区别

在Python中&#xff0c;__str__和__repr__是两个常见的魔法方法&#xff08;也称为双下方法或dunder方法&#xff09;&#xff0c;它们用于定义对象的字符串表示形式。它们的主要区别在于它们的用途和使用场景。 __str__ 用途&#xff1a;__str__方法用于为用户提供一个易读的…