前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >方法调用:一看就懂,一问就懵?

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

原创
作者头像
用户8639654
修改于 2021-07-30 02:33:21
修改于 2021-07-30 02:33:21
40400
代码可运行
举报
文章被收录于专栏:云计算运维云计算运维
运行总次数:0
代码可运行

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

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

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

解析

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

看到这个前提条件,有没有小伙伴联想到对象的多态性?

没错,就是这样,在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的方法的话,都可以在解析阶段中确定唯一的调用版本,符合这个条件的就是我们上边提到的五类方法。它们在类加载的时候就会把符号引用解析为该方法的直接引用,这些方法可以称为「非虚方法」。与之相反,不是非虚方法的方法是「虚方法」

分派

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

静态分派

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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);
    }
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
hello,guy!
hello,guy!

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

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

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

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

动态分派

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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();
    }

}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
man say hello!
woman say hello!
woman say hello!

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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与模式》一书。根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。

「举例说明」

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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());
    }
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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)调用则可能是静态的也可能是动态的。因此我们把 「解析」「静态分派」 这俩在编译期间就确定了被调用的方法,且在运行期间不变的调用称之为静态链接,而在运行期才确定下来调用方法的称之为动态链接。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
SimpleTuning
Simple JVM Tuning simulation,一些怪异的面试题,深入java虚拟机部分笔记以及书本部分资料摘抄。
一滴水的眼泪
2020/09/24
5050
SimpleTuning
深入理解JVM虚拟机5:虚拟机字节码执行引擎
本文转自:https://www.cnblogs.com/snailclimb/p/9086337.html
Java技术江湖
2019/11/21
5940
深入理解Java虚拟机(字节码执行引擎)
执行引擎是 Java 虚拟机最核心的组成部分之一。「虚拟机」是相对于「物理机」的概念,这两种机器都有代码执行的能力,区别是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,而虚拟机执行引擎是由自己实现的,因此可以自行制定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。
张磊BARON
2019/07/31
6740
深入理解Java虚拟机(字节码执行引擎)
深入理解JVM - 栈帧和分派
关于jvm的大部分内容系列文章都已经讨论过了,这个系列也将近尾声,本文将会讲述关于jvm是如何实现重载和重写的,以及栈桢的内部存放的内容,这部分内容是非常重要的,也是面试有可能问到的一些高频面试点。
阿东
2021/09/10
5610
(JVM)Java虚拟机:手把手带你深入解析 - 静态分派 & 动态分派原理
前言 了解 行为方法分派 有利于在行为分派时时进行一些功能操作 本文全面讲解行为分派的类型:静态 & 动态行为分派,希望你们会喜欢。 目录 1. 知识储备 1.1 分派 定义:确定执行哪个方法 的过程
Carson.Ho
2020/01/13
1.1K0
(JVM)Java虚拟机:手把手带你深入解析 - 静态分派 & 动态分派原理
JVM第七卷---虚拟机字节码执行引擎
物理机的执行引擎是直接建立在处理器,缓存,指令集和操作系统层面的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件限制定制指令集与执行引擎结构体系,能够执行哪些不被硬件直接支持的指令集格式。
大忽悠爱学习
2022/05/10
3480
JVM第七卷---虚拟机字节码执行引擎
重载和重写的底层原理——虚拟机字节码执行引擎
代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。
燃192
2023/02/28
3750
重载和重写的底层原理——虚拟机字节码执行引擎
个人笔记,深入理解 JVM,很全!
刷豆瓣看到《深入理解 JVM》出第三版了,遂买之更新 JVM 知识,本文为笔记,仅供个人 Review
猿天地
2021/11/08
2770
虚拟机字节码执行引擎,JVM的马达图,是爱情呀
首先我们要知道,虚拟机是相对于物理机而言,这点毋庸置疑。冒然的讲执行引擎可能会觉得这个东西很突兀,让我们来简单回顾一下JVM的架构图,看看执行引擎所处的位置:
阿甘的码路
2020/09/18
7480
虚拟机字节码执行引擎,JVM的马达图,是爱情呀
深入理解Java虚拟机-虚拟机执行子系统
文章目录类文件结构概述无关性的基石Class类文件的结构魔数与Class文件的版本常量池访问标志运维
Java架构师必看
2021/07/19
3760
深入理解Java虚拟机-虚拟机执行子系统
Java中的方法调用分析!详细解析静态分派和动态分派的执行过程
方法调用 在程序运行时,进行方法调用是最普遍,最频繁的操作 方法调用不等于方法执行: 方法调用阶段唯一的任务就是确定被调用的方法版本,即调用哪一个方法 不涉及方法内部的具体运行过程 Class文件的编译过程不包括传统编译中的连接步骤 Class文件中的一切方法调用在Class文件里面存储的都是符号引用,而不是方法在在实际运行时内存布局中的入口地址,即之前的直接引用: 这样使得Java具有更强大的动态扩展能力 同时也使得Java方法调用过程变得相对复杂 需要在类加载期间,甚至会到运行期间才能确定目标方法的
攻城狮Chova
2022/01/22
7790
《深入理解 JVM 3ed》读书笔记
来源:yinzige.com/2020/03/08/ understanding-jvm-3ed/
芋道源码
2021/10/27
3630
深入浅出JVM(五)之Java中方法调用
本篇文章将围绕Java中方法的调用,深入浅出的说明方法调用的指令、解析调用以及分派调用等
菜菜的后端私房菜
2024/10/08
1680
栈帧之操作数栈(Operand Stack)和动态链接(Dynamic Linking)解读
每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的 操作数栈,也可以称之为表达式栈(Expression Stack)
一个风轻云淡
2023/10/15
3790
栈帧之操作数栈(Operand Stack)和动态链接(Dynamic Linking)解读
详解java虚拟机方法调用
方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。
老马的编程之旅
2022/06/23
3780
深入理解 JVM 之——字节码指令与执行引擎
对于 C 语言从程序到运行需要经过编译的过程,只有经历了编译后,我们所编写的代码才能够翻译为机器可以直接运行的二进制代码,并且在不同的操作系统下,我们的代码都需要进行一次编译之后才能运行。
浪漫主义狗
2023/09/07
5710
深入理解 JVM 之——字节码指令与执行引擎
Java 多态:深入解析 方法重写(Override) 、重载(Overload)及其区别
a. 消除同一类型之间的耦合关系 b. 使得不同对象 对于同一行为 具备多种表现形式
Carson.Ho
2019/02/22
1.3K0
《深入理解Java虚拟机》读书笔记(七)–虚拟机字节码执行引擎(上)
用于存放方法参数和方法内定义的局部变量。在编译阶段,就在方法表的Code属性的max_locals数据项确定了方法所需的局部变量表最大空间。其容量以变量槽(slot)为最小单位,虚拟机规范没有明确规定一个slot应占用的空间大小,只是有导向性地说每个slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,这8种数据类型都可以使用32位或更小的内存来存放,但是也允许slot的长度可以随着处理器、操作系统或虚拟机的不同而变化,只要保证即使使用64位的内存空间去实现一个slot,虚拟机仍然要使用对齐和补白的手段让slot在外观上看起来与32位虚拟机中的一致。
Java架构师必看
2021/11/29
4390
Java方法调用(虚拟机字节码执行引擎)
JAVA方法调用属于虚拟机字节码执行引擎的一部分,执行引擎,可以简单的理解为它用来接收输入的Class文件,按照字节码进行处理程序,然后输出执行结果。
shysh95
2021/03/16
3880
Java方法调用(虚拟机字节码执行引擎)
JVM简介—3.JVM的执行子系统
字节码是各种不同平台虚拟机与所有平台都能统一使用的程序存储格式,所以字节码(ByteCode)是构成平台无关性的基石,是语言无关性的基础。
东阳马生架构
2025/03/11
1110
推荐阅读
相关推荐
SimpleTuning
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验