JVM理论:(三/4)方法调用

  本文主要总结虚拟机调用方法的过程是怎样的,JAVA虚拟机里面提供了5条方法调用的字节码指令。分别如下:

  invokestatic:调用静态方法

  invokespecial:调用实例构造器<init>方法、私有方法和父类方法。

  invokevirtual:调用所有的虚方法。

  invokeinterface:调用接口方法,会在运行时期再确定一个实现此接口的对象。

  invokedynamic:现在运行时期动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条指令,分派逻辑都是固化在虚拟机里面的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

  

  方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即真正要调用哪一个方法),暂时还不涉及方法内部的具体运行过程。一切方法调用在Class文件里面存储的都是符号引用,而不是方法在实际运行时内存布局中的入口地址,这个特性给Java带来了更强大的动态扩展能力,但也使得方法调用过程变得相对复杂起来,需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。

一、解析  

  所有方法调用中的目标方法在Class文件里都是一个常量池中的符号引用,有这样一种情况,在编译器进行编译时就有一个可确定的调用版本,并且这个方法的调用版本在运行期间是不可变的,对于这样的方法,在类加载的解析阶段就会将符号引用转化为直接引用。对这类方法的调用过程称为解析,称这些方法为非虚方法。

  非虚方法(在解析阶段就可以确定唯一的调用版本)包括:

  (1)能被invokestatic和invokespecial指令调用的方法,有静态方法、私有方法、实例构造器、父类方法4类。对于静态方法、私有方法,前者与类型直接关联,后者在外部不可被访问,这就决定了它们都不可能通过继承或别的方式重写其版本。静态方法和私有方法为什么不能实现多态呢?分享这两篇文章https://blog.csdn.net/zhouhong1026/article/details/19114589https://blog.csdn.net/zhousenshan/article/details/51222908。  

  (2)被final修饰的方法,虽然final方法使用invokevirtual指令调用,但由于它无法被覆盖,没有其他版本,所以也无须对方法接收者进行多态选择。

  解析调用一定是一个静态的过程,在编译期间就可以完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期去完成。

二、分派

  解析调用描述的是那些在编译期就能确定唯一调用版本的方法,但对于那些有多个版本的方法,调用时又是如何确定该调用方法的哪一个版本呢?

  分派调用可能是静态的也可能是动态的,根据分派依据的宗量数可分为单分派和多分派,因此分派可分为:静态单分派、静态多分派、动态单分派、动态多分派。分派涉及到Java面向对象3个基本特征:继承、封装、多态。特别是多态性特征的一些体现——重载、重写。

1、静态分派

  所有依赖变量的静态类型来定位方法执行版本的分派动作称为静态分派,静态分派的典型应用是方法重载,静态分派发生在编译阶段。先通过以下例代码来说明方法重载。

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 man");
    }
    public void sayHello(Woman guy){
        System.out.println("Hello woman");
    }

    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则称为变量的实际类型,静态类型在编译期就可知,而实际类型变化的结果在运行期才可以确定。main()里面两次sayHello()方法调用,在方法接收者已经确定是对象“sr”的前提下,使用哪个重载版本,就完全取决于传入参数的数量和数据类型,代码中刻意地定义了两个静态类型相同但实际类型不同的变量,但Javac编译器在重载时是根据参数的静态类型决定使用哪个重载版本,所以选择了sayHello(Human)作为调用目标。  

  编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本也不是唯一的,往往只能确定一个更加合适的版本。

  解析与分派这二者并不是二选一的排他关系,它们是在不同层次上去筛选、确定目标方法的过程。例如,静态方法会在类加载期就进行解析,但静态方法也可能拥有重载版本,选择重载版本的过程也是通过静态分派完成的。

2、动态分派

  动态分派和“重写”有着密切联系,以下列代码为例来说明重写。

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

  以上两个静态类型同是Human的变量man和woman,两个变量的实际类型不同,最终执行的目标方法也不同。两条调用都用到了invokevirtual指令,invokevirtual指令的运行解析过程大致分为以下几个步骤:

 1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C;

2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError。

3)否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。

4)如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

 由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言中方法重写的本质,我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派

3、单分派与多分派

  方法的接收者与方法的参数统称为方法的宗量,根据分派基于多少宗量,可以将分派划分为单分派和多分派两种。像上面两个例子如果既需要确定方法调用者的实际类型,又需要通过参数的静态类型来确定使用哪种重载方法,属于多分派,若只以一个宗量对目标方法进行选择就是单分派。

4、虚拟机是如何具体实现动态分配的

  动态分派对方法版本选择时不会在运行期对类的方法元数据进行频繁搜索以找到合适的目标方法,而是为类在方法区中建立一个虚方法表,如果是接口,在invokeinterface执行时也会用到接口方法表,使用虚方法表索引来代替元数据查找以提高性能。

  

  虚方法表中存放着各个方法的实际入口地址,如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。如上图所示,Son重写了来自Father的全部方法,因此Son的方法表没有指向Father类型数据的箭头。但是Son和Father都没有重写来自Object的方法,所以他们的方法表中所有从Object继承来的方法都指向了Object的数据类型。方法表一般在类加载的连接阶段进行初始化,准备了类变量的初始值后,虚拟机会把该类的方法表也初始化完毕。

三、 invokedynamic对动态类型语言的支持

  像Java、C++等这些在编译期就进行类型检查过程的语言属于静态类型语言,像PHP、Python、Javascript等这些在运行期才进行类型检查的语言为动态类型语言。

  JDK1.7以前,4条方法调用指令invokevirtual、invokespecial、invokestatic、invokeinterface的第一个参数都是被调用的方法的符号引用,符号引用包含了此方法定义在哪个具体类型之中、方法的名字以及参数顺序、参数类型和方法返回值等信息,通过这个符号引用,虚拟机就可以翻译出这个方法的直接引用。方法的符号引用在编译时产生,而动态类型语言只有在运行期才能确定接收者类型。所以这4条指令无法很好地支持动态语言。

  JDK1.7后,为了更好地支持动态类型语言,引入了第五条方法调用的字节码指令invokedynamic。某种程度上可以说invokedynamic指令与MethodHandle机制的作用是一样的。invokedynamic指令与前面4条“invoke*”指令的最大差别就是它的分派逻辑不是由虚拟机决定的,而是由程序员决定。在MethodHandles.Lookup中的三个方法findStatic()、findVirtual()、findSpecial()正是为了对应于invokestatic、invokevirtual & invokeinterface和invokespecial这几条字节码指令的执行权限校验行为。

import static java.lang.invoke.MethodHandles.lookup;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
public class Test{
    class GrandFather{
        void thinking(){
            System.out.println("i am grandfather");
        }
    }
    class Father extends GrandFather{
        void thinking(){
            System.out.println("i am father");
        }
    }
    class Son extends Father{
        void thinking(){
            try{
                MethodType mt=MethodType.methodType(void.class);
                MethodHandle mh=lookup().findSpecial(GrandFather.class, "thinking", mt, getClass());  
                mh.invoke(this);
            }catch(Throwable e){
            }
        }
    }
    public static void main(String[] args){
        (new Test().new Son()).thinking();
    }
}

  上面这段代码,可以通过“super”关键字很方便地调用到父类中的方法,但如果要访问祖类的方法呢?在JDK 1.7之前,在Son类的thinking()方法中无法获取一个实际类型是GrandFather的对象引用,而invokevirtual指令的分派逻辑就是按照方法接收者的实际类型进行分派,这个逻辑是固化在虚拟机中的,程序员无法改变。在JDK 1.7后,通过invokedynamic的支持可以用上面的代码实现。

  

原文地址:https://www.cnblogs.com/zjxiang/p/9218164.html

时间: 2024-10-24 21:05:24

JVM理论:(三/4)方法调用的相关文章

JVM(十二):方法调用

JVM(十二):方法调用 在 JVM(七):JVM内存结构 中,我们说到了方法执行在何种内存结构上执行:Java 方法活动在虚拟机栈中的栈帧上,栈帧的具体结构在内存结构中已经详细讲解过了,下面就让我们来看一下 方法是如何调用的. 方法调用 首先,我们要明白一个基础性概念:方法调用并不是方法执行.其只是确定该调用哪一个方法而已(多态的影响,选择方法的不同版本).并且因为 Java 调用的动态性,有些方法需要在类加载阶段动态解析,这也为 JVM 解析符号引用成直接引用提供了难度. 解析 在 JVM(

JVM是如何进行方法调用的

在我们平时的工作学习中写java代码时,如果我们在同一个类中定义了两个方法名和参数类型都相同的方法时,编译器会直接报错给我们.还有在代码运行的时候,如果子类定义了一个与父类完全相同的方法的时候,父类的方法就会被覆盖,(也就是我们平时说的重写).那么,jvm虚拟机是如何精确识别目标方法的. 重载.重写与多态 重载:方法名相同而参数类型不相同的方法之间的关系. 重写:方法名相同并且参数类型也相同的方法之间的关系. 这两个概念我们耳熟能详,那么重载和重写是如何判断的呢? 重载: 重载的方法在编译期间就

04 JVM是如何执行方法调用的(下)

虚方法调用 Java 里所有非私有实例方法调用都会被编译成 invokevirtual 指令,而接口方法调用会被编译成 invokeinterface 指令.这两种指令,均属于 Java 虚拟机中的虚方法调用. 动态绑定:Java 虚拟机需要根据调用者的动态类型,来确定虚方法调用的目标方法. 静态绑定:调用静态方法的 invokestatic 指令,以及用于调用构造器,私有实例方法和超类非私有实例方法的 invokestatic 指令.如果虚方法调用指向一个标记为 final 的方法,那么 Ja

JVM是如何执行方法调用的?(下)

前言....... abstract class 乘客 { abstract void 出境 (); @Override public String toString() { ... } } class 外国人 extends 乘客 { @Override void 出境 () { /* 进外国人通道 */ } } class 中国人 extends 乘客 { @Override void 出境 () { /* 进中国人通道 */ } void 买买买 () { /* 逛免税店 */ } } 乘

JVM方法调用

当我们站在JVM实现的角度去看方法调用的时候,我们自然会想到一种分类: 1.编译代码的时候就知道是哪个方法,永远不会产生歧义,例如静态方法,private方法,构造方法,super方法. 2.运行时才能确定是哪个方法,这也正是多态的实现原理. 对于第一种方法的调用,有2个字节码指令:invokestatic,invokespecial invokestatic:调用static方法(不需要通过类的实例就可以调用),这很好理解.静态方法属于整个类型,就一份,没有歧义. invokespecial:

JVM方法调用(invokevirtual)

在java代码运行期间,方法间的调用可以说是最为频繁的了,那么这些方法间的调用在底层的虚拟机又做了什么事情呢?现在就让我们揭开那道神秘的面纱. JVM调用方法有五条指令,分别是invokestatic,invokespecial,invokevirtual,invokeinterface,invokedynamic.invokestatic用来调用静态方法:invokespecial用来调用私有方法,父类方法(super.),类构造器方法:invokeinterface调用接口方法:invoke

UFT脚本调用外部VBS函数的三种方法

第一种方法:ExecuteFile,利用该函数将外部vbs动态地加载进来,使测试脚本可以调用vbs文件的所有函数,调用语句写在下方,顺序不能颠倒,否则会报错.这个方法适用于QTP任何版本,但有个缺点,代码调试时不能跟踪到外部vbs的函数中,多少有些不便. ExecuteFile File File  String  The absolute or relative path of the file to execute. Example: ExecuteFile "c:\out.vbs"

Struts2学习之路(三)—— Action方法调用

上一篇(http://www.cnblogs.com/denisechen/p/4644275.html)提到,action执行的时候并不一定要执行execute方法.那么该如何决定要调用的方法?有以下三种方式: 1.使用method属性指定具体方法 这种方法会使得创建的action非常多. 1 <action name="order" class="com.action.Order" method="add"> 2 <resu

YbSoftwareFactory 代码生成插件【二十五】:Razor视图中以全局方式调用后台方法输出页面代码的三种方法

上一篇介绍了 MVC中实现动态自定义路由 的实现,本篇将介绍Razor视图中以全局方式调用后台方法输出页面代码的三种方法. 框架最新的升级实现了一个页面部件功能,其实就是通过后台方法查询数据库内容,把查询结果的 HTML 代码呈现到 Razor 视图中,考虑到灵活性,需要能在任意 Razor 视图中调用该方法,这样任意 Razor 页面都能以统一的方式方便地共享该页面部件的 HTML 内容,这对于代码的重用性和可维护性都是非常有必要的. 为实现上述要求,本文介绍如下可供选择的三种方式.   1.