JVM是如何进行方法调用的

  在我们平时的工作学习中写java代码时,如果我们在同一个类中定义了两个方法名和参数类型都相同的方法时,编译器会直接报错给我们。还有在代码运行的时候,如果子类定义了一个与父类完全相同的方法的时候,父类的方法就会被覆盖,(也就是我们平时说的重写)。那么,jvm虚拟机是如何精确识别目标方法的。

重载、重写与多态

重载:方法名相同而参数类型不相同的方法之间的关系。

重写:方法名相同并且参数类型也相同的方法之间的关系。

这两个概念我们耳熟能详,那么重载和重写是如何判断的呢?

重载:

重载的方法在编译期间就可以完成识别。java编译器会根据所传入参数的声明类型对方法名相同的方法进行选取。

除了同一个类中,如果A继承了B,A中定义了与B中的非私有方法同名的方法,而且这两个方法的参数类型不同,那么A和B类同样构成了重载

重写:

重写方法的判断是在运行期间才可以完成识别的。

我们都知道多态是java面向对象语言的三大特性之一。而方法的重写,就是最能体现多态的一种方式:它允许子类在继承父类部分特性的同时,拥有自己独特的行为。

举个简单的例子帮大家理解一下多态:

比如我们按下b这个键,在dota中代表的是敌法师的blink技能,在lol中是回城,在网游里又成了背包。对于不同的对象拥有不同的行为,这就是多态。

静态绑定与动态绑定

接下来,我们来看一下jvm是如何识别目标方法的。

刚才我们说到,重载方法的区分在编译阶段已经完成了,那么我们就可以认为在java虚拟机中不存在重载这一概念。因此,重载也可以被称为静态绑定,而重写则被称为动态绑定。

在jvm中,我们有5种方法调用的指令,分别是:

invokestatic:调用静态方法;

invokespecial:调用实例构造方法,私有方法和父类方法,以及使用super关键字调用父类的实例方法或构造器;

invokevirtual:调用虚方法(非私有实例方法);

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

invokedynamic:在运行时动态解析出调用点限定符所引用的方法之后,调用该方法(jdk1.8lamada表达式);

这里,我们简单介绍一下这几种指令,对于invokestatic指令和invokespecial指令而言,java虚拟机能够直接识别目标方法,也就是我们所说的静态绑定

invokevirtual和invokeinterface指令则需要在执行的过程中才能找到目标方法,也就是我们所说的动态绑定

总结一下静态绑定和动态绑定的概念就是:

静态绑定:在程序执行之前就已经被绑定、也就是说再编译阶段就已经知道这个方法是属于哪个类的方法。

1.private修饰的方法,不能被子类调用   2.  被final修饰的方法      3.被static修饰的方法

动态绑定:在运行过程中根据调用者的动态类型来识别目标方法的情况。

动态绑定中,我们会记录方法对应的实际引用的地址,也可以理解为索引值,这里我们把它叫做方法表

方法表使用了数组的数据结构,每个数组元素指向了当前类以及其祖先类中非私有的实例方法。

这个数据结构,便是java虚拟机实现动态绑定的关键所在

虚方法

通过这些指令的描述,我们发现虚方法和非虚方法直接决定了静态绑定还是动态绑定,也就决定了是直接用父类的方法还是动态地用子类重写的方法。所以,我们有必要去理解虚方法,并能判断哪些是属于虚方法。

我们先一起看两段代码:

代码1:

class Dota {
    private void play() {
        System.out.println("我喜欢玩dota,哈哈哈~~~");
    }
    void startGame() {
        play();
    }
}

class LoL extends Dota {
    void play() {
        System.out.println("我喜欢玩lol,哈哈哈~~~~");
    }
}

public class Demo1{
    public static void main(String[]args){
        new LoL().startGame();
    }
}

代码二:

class Dota {
     void play() {
        System.out.println("我喜欢玩dota,哈哈哈~~~");
    }

    void startGame() {
        play();
    }
}

class LoL extends Dota {
    void play() {
        System.out.println("我喜欢玩lol,哈哈哈~~~~");
    }
}

public class Demo2{
    public static void main(String[]args){
        new LoL().startGame();
    }
}

  这里,dota是lol的父类(本人是dotaer,哈哈),这两段代码的唯一不同就是代码1的父类的play方法private修饰的,而代码2中的play()方法不是私有的。接下来,我们看下输出结果:

代码1:

我喜欢玩dota,哈哈哈~~~

代码2:

我喜欢玩lol,哈哈哈~~~~

  第一段代码直接调用了父类的play()方法,而第二段代码调用了子类的play()方法

  大家是不是觉得很奇怪,一个私有的修饰符就能使结果不一样吗?

  结合我们之前所说的进行判别,第一段代码应该是静态绑定,第二段代码则是动态绑定

  那么,代码1就是非虚函数,代码2是虚函数。接下来,我们来看下虚函数的概念:

虚函数:除了静态方法之外,声明为final或者private的实例方法是非虚方法。其它(其他非private方法)实例方法都是虚方法。

当子类对象调用重写的方法时,调用的是子类的方法,而不是父类中被重写的方法。

        要想调用父类中被重写的方法,则必须使用关键字super。

代码1由于是private修饰,所以为非虚函数,调用了invokespecial指令。

代码2是虚函数,则调用了invokevirtual指令。

再看一个例子

代码3:

public class Demo2 {
    static abstract class Game {}
    static class Dota extends Game {}
    static class Lol extends Game {}

    public void play(Game game) {
        System.out.println("hello,game");
    }

    public void play(Dota dota) {
        System.out.println("hello,Dota");
    }

    public void play(Lol lol) {
        System.out.println("hello,lol");
    }

    public static void main(String[] args) {
        Game dota = new Dota();
        Game lol = new Lol();
        Demo2 sd = new Demo2();
        sd.play(dota);
        sd.play(lol);
    }
}

输出结果:

hello,game
hello,game

虽然在这里,play方法是虚方法,是动态绑定,但是调用play方法的是Demo2的实例sd,由于Demo2方法没有子类,所以不需要考虑,则直接执行父类的方法。

总结:  

我们介绍了java虚拟机(jvm)是如何执行方法的,我们从我们熟悉的重载和重写切入,了解了静态绑定和动态绑定的概念:

静态绑定:在程序执行之前就已经被绑定、也就是说再编译阶段就已经知道这个方法是属于哪个类的方法。

动态绑定:在运行过程中根据调用者的动态类型来识别目标方法的情况

并且知道了jvm中用方法表来维护非私有实例方法与其索引值的对应关系来实现动态绑定。

接着,我们了解了jvm中调用方法的5个指令,并且通过静态绑定和动态绑定的概念对其进行归类,

静态绑定:

invokestatic:调用静态方法;

invokespecial:调用实例构造方法,私有方法和父类方法;

动态绑定:

invokevirtual:调用虚方法;

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

invokedynamic:在运行时动态解析出调用点限定符所引用的方法之后,调用该方法;

归类中通过虚函数的概念,使我们对jvm中方法的调用加以更深的了解。

虚函数:除了静态方法之外,声明为final或者private的实例方法是非虚方法。其它(其他非private方法)实例方法都是虚方法。

原文地址:https://www.cnblogs.com/GodHeng/p/10234917.html

时间: 2024-11-05 18:59:04

JVM是如何进行方法调用的的相关文章

JVM(十二):方法调用

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

关于JVM中方法调用的相关指令,以及解析(Resolution)和分派(Dispatch)的解释——重载的实现原理与重写的实现原理

JVM中相关方法的调用的指令 invokestatic 调用静态方法. invokespecial 用于调用构造器方法<init>.私有方法.父类方法. invokevirtual 用于调用类的所有虚方法. invokeinterface 用于调用接口方法. 解析(resolution)与分派(dispatch) 解析 解析调用一定是个静态的过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变 为可确定的直接引用,不会延迟到运行期再去完成. 下面我们看一段代码: /**

JVM学习笔记(二)--方法调用之静态分配和动态分配

本篇文章从JVM的角度来理解Java学习中经常提到的重载和重写. 方法调用:方法调用不等同于方法执行,在Java虚拟机中,方法调用仅仅是为了确定调用哪个版本的方法.方法调用分为解析调用和分派.解析调用一定是静态的,而分派可以是静态的,也可以是动态的.我们这里只介绍分派中的静态分配和动态分配. 静态分配:所有依赖静态类型来定位方法执行版本的分派动作称为静态分配. 下面看个例子,顺便来猜一下结果(面试中经常遇到): 1 class Human { 2 3 } 4 5 class Man extend

Jvm(65),方法调用----静态分派调用

我们先来看一下下面的例子: package demo.jvm.test8? public class Demo2 { /** 方法静态分派演示 * @author zzm */ static abstract class Human { } static class Man extends Human { } static class Woman extends Human { } public void sayHello(Human guy) { System.out.println("hel

Jvm(64),方法调用----解析

继续前面关于方法调用的话题,所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的.换句话说,调用目标在程序代码写好.编译器进行编译时就必须确定下来.这类方法的调用称为解析(Resolution). 换句话就是说在写好代码之后通过eclipse编译之后,编译出来的结果是不会再变化了. 在Java语言中符合"