个人主页:阿Q说代码
‍♂️作者简介:公众号阿Q说代码作者(期待你的关注)、infoQ签约作者、CSDN后端范畴新星创作者
技能方向:专心于后端技能栈分享:JVM、数据库、中间件、微服务、Spring全家桶

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

面试:一文带你彻底搞懂方法调用的底层原理

首先咱们要清晰一个概念,此处的办法调用并不是办法中的代码被履行,而是要确认被调用办法的版别,即最终会调用哪一个办法。

上篇文章中咱们了解到,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的办法的话,都能够在解析阶段中确认仅有的调用版别,符合这个条件的便是咱们上边说到的五类办法。它们在类加载的时候就会把符号引证解析为该办法的直接引证,这些办法能够称为非虚办法。与之相反,不是非虚办法的办法是虚办法

面试:一文带你彻底搞懂方法调用的底层原理

分配

假如咱们在编译期间没有将办法的符号引证转化为直接引证,而是在运转期间依据办法的实践类型绑定相关的办法,咱们把这种办法的调用称为分配。其间分配又分为静态分配和动态分配。

静态分配

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

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)调用则可能是静态的也可能是动态的。因而咱们把 解析静态分配 这俩在编译期间就确认了被调用的办法,且在运转期间不变的调用称之为静态链接,而在运转期才确认下来调用办法的称之为动态链接。

咱们把在静态链接进程中的转化成为前期绑定,将动态链接进程中的转化称之为晚期绑定。

跪求一键三连,期望靓仔在评论区打出“老铁666”,鼓舞一下阿Q。

好看的皮囊千篇一律,有趣的魂灵万里挑一,让咱们在冷漠的城市里相互温暖,我是阿Q,咱们下期再会!