方法调用:一看就懂,一问就懵?

2023-11-14

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

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

上篇文章中我们了解到,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{
        @Override
        protected void sayHello() {
            System.out.println("man say hello!");
        }
    }
    static class Woman extends Human{
        @Override
        protected 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)调用则可能是静态的也可能是动态的。因此我们把 解析静态分派 这俩在编译期间就确定了被调用的方法,且在运行期间不变的调用称之为静态链接,而在运行期才确定下来调用方法的称之为动态链接。

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

看到这,方法的调用你搞懂了吗?如果你还有什么困惑的话,可以关注gzh“阿Q说代码”,也可以加阿Q好友qingqing-4132,阿Q期待你的到来!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

方法调用:一看就懂,一问就懵? 的相关文章

  • 如何找出一个对象有多少个引用? [复制]

    这个问题在这里已经有答案了 是否可以和 或容易地找出任意对象有多少个传入引用 也就是说 有多少对象引用它 提前致谢 简短的回答是 你自己数一下 StackOverflow 的另一个问题有一些有用的答案和资源 是否可以获得对象引用计数 htt
  • CATALINA_OPTS 与 JAVA_OPTS - 有什么区别?

    我试图找出 Apache Tomcat 变量之间的区别 CATALINA OPTS and JAVA OPTS in SO http stackoverflow com并惊讶地发现这里还没有发布问题 答案 所以我想在发现差异后在这里分享 带
  • JVM 退出代码 1073807364 的原因是什么?

    我构建了一个基于 RCP 的应用程序 我的一位用户在 Windows XP Sun JVM 1 6 0 12 上运行时 应用程序完全崩溃了 应用程序运行两天后 这不是新版本或其他任何东西 他得到了漂亮的灰色 JVM 强制退出框 退出代码 1
  • 有什么方法可以显示正在运行的 JVM 中使用的标志吗?

    尽管我们已经为应用程序显式设置了许多 JVM 标志 但很难知道是否 1 布尔标志默认已打开 默认值在 JDK JRE 次要更新之间发生了变化 2 一个标志否定另一个标志 3 特定系统上给定任意标志的默认值是什么 由 Java 人体工程学设置
  • JVM CPU 峰值故障排除

    我们在其中一台应用程序服务器上发现了一个有趣的 尽管相当严重 问题 在某个时间点 运行 Web 应用程序的 JVM 的 CPU 使用率开始上升 并持续上升 直到应用程序最终减慢到爬行 修复此问题的唯一方法是重新启动应用程序服务器软件 应用服
  • IntelliJ 调试:暂停整个虚拟机,然后进入单线程

    我正在调试一个具有大量线程的应用程序 我的断点设置为暂停整个虚拟机 当线程遇到其中一个断点时 我想使用 Step Over 但这似乎会恢复整个虚拟机 直到该步骤完成 如果我可以只单步执行到达断点的单个线程 那确实会有帮助 在 Intelli
  • 如何使用IntelliJ IDEA ThreadDumpVisualizer插件分析Java线程转储

    我正在寻找使用一些线程转储分析器来分析 Java 线程转储并安装了ThreadDumpVisualizerIntelliJ IDEA 插件 但不知道如何使用它 插件页面 https plugins jetbrains com plugin
  • JVM 规范更新

    JVM 规范第 2 版的日期是 1999 年 自那时以来 我应该考虑学习哪些重要更新 如动态调用 这当然是为了了解现代 JVM 实现的内部原理 特别是 HotSpot 访问此链接http wikis sun com display HotS
  • 估计 64 位 Java 中最大安全 JVM 堆大小

    在分析存在一些问题的 64 位 Java 应用程序的过程中 我注意到分析器本身 YourKit 正在使用真正大量的内存 我在 YourKit 启动脚本中得到的是 JAVA HEAP LIMIT Xmx3072m XX PermSize 25
  • 哪种语言(在 JVM 上运行)最适合创建 DSL?

    我们需要创建复杂的固定长度和可变长度字符串 这些字符串可能代表客户资料 订单等 你们建议使用哪种基于 JVM 的编程语言 想法是让最终用户使用此 DSL 创建字符串 所以我正在寻找验证 代码完成等 Groovy http docs code
  • 如何查看JVM中JIT编译的代码?

    有什么方法可以查看 JVM 中 JIT 生成的本机代码吗 一般用法 正如其他答案所解释的 您可以使用以下 JVM 选项运行 XX UnlockDiagnosticVMOptions XX PrintAssembly 根据特定方法进行过滤 您
  • 最近有关于 JVM 的书吗? [关闭]

    就目前情况而言 这个问题不太适合我们的问答形式 我们希望答案得到事实 参考资料或专业知识的支持 但这个问题可能会引发辩论 争论 民意调查或扩展讨论 如果您觉得这个问题可以改进并可能重新开放 访问帮助中心 help reopen questi
  • 什么触发了java垃圾收集器

    我对 Java 中垃圾收集的工作原理有点困惑 我知道当不再有对某个对象的实时引用时 该对象就有资格进行垃圾回收 但是如果它有对实时对象的引用怎么办 可以说我有一个节点集合 它们再次引用更多节点 List 1 gt Node a gt Nod
  • 在intellij中为java启用ssl调试

    从我的问题开始 上一期尝试通过 tls ssl 发送 java 邮件 https stackoverflow com questions 39259578 javamail gmail issue ready to start tls th
  • Java 接口合成方法生成,同时缩小返回类型

    我有 2 个接口和 2 个返回类型 interface interfaceA Publisher
  • JVM 是否会内联对象的实例变量和方法?

    假设我有一个非常紧密的内部循环 每次迭代都会访问和改变一个簿记对象 该对象存储有关算法的一些简单数据 并具有用于操作它的简单逻辑 簿记对象是私有的和最终的 并且它的所有方法都是私有的 最终的和 inline 下面是一个示例 Scala 语法
  • 如何判断我是在 64 位 JVM 还是 32 位 JVM 中运行(在程序内)?

    如何判断应用程序运行的 JVM 是 32 位还是 64 位 具体来说 我可以使用哪些函数或属性来在程序中检测到这一点 对于某些版本的 Java 您可以使用标志从命令行检查 JVM 的位数 d32 and d64 java help d32
  • Scala REPL 中的递归重载语义 - JVM 语言

    使用 Scala 的命令行 REPL def foo x Int Unit def foo x String Unit println foo 2 gives error type mismatch found Int 2 required
  • 如何在Java中计算对象的数字年龄[关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 我想知道Java中对象的年龄 当我们使用new关键字时 Java中用户定义的对象被创建 但是什么时候它会被销毁 是跨越JVM的perm
  • Java 语言中不可用的字节码功能

    当前 Java 6 是否有一些事情可以在 Java 字节码中完成而在 Java 语言中无法完成 我知道两者都是图灵完备的 所以将 可以做 理解为 可以做得更快 更好 或者只是以不同的方式 我正在考虑额外的字节码 例如invokedynami

随机推荐

  • Thrift之TProtocol类体系原理及源码详细解析之JSon协议类TJSONProtocol

    我的新浪微博 http weibo com freshairbrucewoo 欢迎大家相互交流 共同提高技术 JSON JavaScriptObjectNotation 是一种数据交换格式 是以JavaScript为基础的数据表示语言 是在
  • vant框架DropdownMenu 下拉菜单组件在小程序中的应用

    vant框架DropdownMenu 下拉菜单组件在小程序中的应用 官方文档实例
  • Grafana Kubernetes部署(rancher)

    1 相关资源导航 https blog csdn net zyj81092211 article details 122917786 2 环境介绍 kubernetes版本 v1 23 4 rancher版本 v2 6 3 容器相关环境配置
  • 获取服务器信息失效,获取服务器时间失败

    获取服务器时间失败 内容精选 换一换 安装完Mind Studio后 如果用户进行编译运行相关操作 则需要参见该章节 将硬件环境的lib库同步到Mind Studio安装服务器 已经完成安装 请确保DDK版本号与硬件环境所安装的软件包版本号
  • IO(输入/输出)

    用户态和内核态 用户态 用来运行应用程序 不能直接对操作系统进行调用 而是需要切换到内核态对操作系统进行操作 内核态 直接访问操作系统资源或运行操作系统程序 例如程序要保存一个文件到硬盘 在程序执行的用户态 是直接操作磁盘的 只有切换到内核
  • Socket编程之聊天室

    1 单线程模式 创建服务端 第一步 准备地址和端口 第二步 创建一个ServerSocket对象 第三步 等待客户端连接 最后一步 数据接收和发送 public class SingleThreadServer public static
  • Linux线程同步

    1 同步 同步即协同步调 按预定的先后次序运行 线程同步 指一个线程发出某一功能调用时 在没有得到结果之前 该调用不返回 同时其它线程为保证数据一致性 不能调用该函数 解决同步的问题 加锁 2 数据混乱原因 1 资源共享 独享资源则不会 2
  • ubuntu-16.04 安装虚拟机工具时报错

    2019独角兽企业重金招聘Python工程师标准 gt gt gt root alex virtual machine home alex Desktop vmware tools distrib vmware install pl ope
  • Mathtype公式编辑软件 安装教程

    文章目录 1 MathType公式编辑器 介绍 2 MathType 安装 2 1 下载包 2 2 安装源程序 2 3 安装补丁 4 验证是否安装成功 我们再写论文时 一般都明确要求 公式必须用MathType编辑 所有公式必须在MathT
  • 什么是软件外包公司?要不要去外包公司?

    关注后回复 进群 拉你进程序员交流群 作者丨土豆居士 来源丨一口Linux ID yikoulinux 一 什么是外包 软件外包分为 人力外包和项目外包两个方向 1 劳务派遣 指的是把员工外派到对应的用工企业打 短工 比如很多工程师虽然签约
  • SpringBoot总结

    一 SpringBoot简介 1 入门案例 SpringMVC的HelloWord程序大家还记得吗 SpringBoot是由Pivotal团队提供的全新框架 其设计目的是用来简化Spring应用的初始搭建以及开发过程 原生开发SpringM
  • 153个!PCB板上的字母符号都代表啥?一图带你搞懂!

    PCB板是基于电路设计图而生产的 看过电路设计图的小伙伴都会知道 上面有各种物理电学标准符号 通过分析电路设计图 可以得知将使用哪些电子元器件 各元器件之间的关系 以及该电路具备哪些性能 为此 小编在网络上搜集了一些电工电路图常用的字母符号
  • 石锤!谷歌排名第一的编程语言,死磕这点,程序员都收益

    日本最大的证券公司之一野村证券首席数字官马修 汉普森 在Quant Conference上发表讲话 用Excel的人越来越少 大家都在码Python代码 甚至直接说 Python已经取代了Excel 事实上 为了追求更高的效率和质量 他们开
  • 数据结构与算法——马踏棋盘(c++栈实现)

    马踏棋盘问题是旅行商 TSP 或哈密顿问题 HCP 的一个特例 在国际棋盘棋盘上 用一个马按照马步跳遍整个棋盘 要求每个格子都只跳到一次 最后回到出发点 这是一个 NP问题 通常采用回溯法或启发式搜索类算法求解 在此采用栈进行回溯法求解 i
  • 嵌入式:驱动开发 Day4

    作业 通过字符设备驱动分步注册方式编写LED驱动 完成设备文件和设备的绑定 驱动程序 myled c include
  • OpenCASCADE:在 Android 上使用 OCCT AndroidQt 示例进行 C/C++ 开发

    OpenCASCADE 在 Android 上使用 OCCT AndroidQt 示例进行 C C 开发 在 Android 平台上进行 C C 开发是一项具有挑战性的任务 然而 通过使用 OpenCASCADE OCCT 库和 Andro
  • java linux mac,Java - 获取Linux系统的MAC地址

    I m trying to get the MAC address of a linux system with this code try ip InetAddress getLocalHost NetworkInterface netw
  • Jenkins-CI 远程代码执行漏洞(CVE-2017-1000353)

    Jenkins Jenkins是一个开源软件项目 是基于Java开发的一种持续集成工具 用于监控持续重复的工作 旨在提供一个开放易用的软件平台 使软件项目可以进行持续集成 漏洞描述 该漏洞存在于使用HTTP协议的双向通信通道的具体实现代码中
  • ES自己手动高亮

    背景 es的高亮真的是一言难尽 经常出现各种各样的高亮异常 如 高亮错位 高亮词错误等等 而且 用wildcardQuery 等 也无法高亮 可能是我技术不精吧 总是调不好这玩意 因此决定手写高亮 废话不多说 直接上代码 1 第一步 处理高
  • 方法调用:一看就懂,一问就懵?

    方法调用是不是很熟悉 那你真的了解它吗 今天就让我们来盘一下它 首先大家要明确一个概念 此处的方法调用并不是方法中的代码被执行 而是要确定被调用方法的版本 即最终会调用哪一个方法 上篇文章中我们了解到 class字节码文件中的方法的调用都只