从虚拟机指令执行的角度分析JAVA中多态的实现原理

从虚拟机指令执行的角度分析JAVA中多态的实现原理

前几天突然被一个“家伙”问了几个问题,其中一个是:JAVA中的多态的实现原理是什么?

我一想,这肯定不是从语法的角度来阐释多态吧,隐隐约约地记得是与Class文件格式中的方法表有关,但是不知道虚拟机在执行的时候,是如何选择正确的方法来执行的了。so,趁着周末,把压箱底的《深入理解Java虚拟机》拿出来,重新看了下第6、7、8章中的内容,梳理一下:从我们用开发工具(Intellij 或者Eclipse)写的 .java 源程序,到经过javac 编译成class字节码文件,再到class字节码文件被加载到虚拟机并最终根据虚拟机指令执行选择出正确的(多态)方法执行的整个过程。

在讨论的多态(一般叫运行时多态)的时候,不可避免地要和重载(Overload)进行对比,为什么呢?因为这涉及到一种方法调用方式----分派(分派这个名字来源于 深入理解Java虚拟机 第8章8.3.2节)

先从源代码(语法)的角度看看二者的区别:

  • 重载(Overload)
  • 重写(Override),或者叫运行时多态,这是本文主要要讨论的内容。

?

先来看看重载,(代码来源于书中)

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);//hello, guy
    sr.sayHello(woman);//hello, guy
    }
}

再来看看重写(Override)

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();//man say hello
    woman.sayHello();//woman say hello
    }
}

在StaticDispatch.java 中,并不存在子类方法、父类方法。只有StaticDispatch.java的sayHello方法,即:sayHello 方法的接收者都是 StaticDispatch sr 对象,需要根据sayHello方法的参数类型来确定,具体执行下面这三个方法中的哪一个方法:

    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");
    }

而在DynamicDispatch.java中,首先有一个父类Human,它有一个sayHello方法,然后有两个子类:Woman、Man,它们分别@Override 了父类中的sayHello方法,也就是说:子类重写了父类中的方法。

上而就是从(源代码)语法的角度 描述了一下 重载(Overload) 和 重写(Override 或者叫运行时多态)的区别。程序要想执行,先要将源代码编译成字节码文件。

接下来看一下,二者在字节码文件上的不同

首先javac 命令将 StaticDispatch.java 和 DynamicDispatch.java编译成 class文件,然后使用分别使用下面命令输出这两个文件字节码的内容:

javap -verbose StaticDispatch

上面截取的是 StaticDispatch.java main方法中的方法表中的内容。方法表的结构 可参考书中第6.3.6小节的描述。

main方法中的代码,经过编译器编译成字节码指令后,存放在方法属性表集合中的一个名为 "Code" 的属性里面。

StaticDispatch 的main方法 字节码的执行过程

从上面的序号 26和 序号31 的字节码可以看出:sr.sayHello(man);sr.sayHello(woman); 是由 invokevirtual指令执行的。

而且方法的符号引用都是:Method sayHello:(Lorg/hapjin/dynamic/StaticDispatch$Human;)V

好,那咱就来看看,invokevirtual指令的具体执行过程,看它是如何将符号引用 解析到 具体的方法上的。

因为,覆盖(Override)或者说运行时多态也是通过invokevirtual指令来选择具体执行哪个方法的,因此:invokevirtual指令的解析过程 可以说是JAVA中实现多态的原理吧。

invokevirtual指令的解析过程大致分为以下几个步骤:

1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C
2. 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。
3. 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

因此,第一步,找到操作数栈顶的第一个元素所指的对象的实际类型,这个对象其实就是方法接收者的实际类型,它是StaticDispatch对象sr StaticDispatch sr = new StaticDispatch()

为什么是sr对象呢?比如对于序号26的invokevirtual指令,序号24、25行的两条aload_3 和 aload_1字节码指令 分别是把第四个引用类型的变量推送到栈顶,把第二个引用类型的变量推送到栈顶。而第四个引用类型的变量是StaticDispatch sr 对象;第二个引用类型的变量则是Man类的对象Human man = new Man()

第二步,根据常量 Method sayHello:(Lorg/hapjin/dynamic/StaticDispatch$Human;)V寻找 StaticDispatch类中哪个方法的简单名称和描述符都与该常量相同。

常量Method sayHello:(Lorg/hapjin/dynamic/StaticDispatch$Human;)V的简单名称是 ‘sayHello‘,描述符信息是:返回的类型为空,参数类型为Human,只有一个参数。

而在StaticDispatch.java中一共有三个不同的sayHello方法,它们的简单名称都是‘sayHello‘,而描述符中的参数类型为‘Human‘类型的方法是:

    public void sayHello(Human guy) {
    System.out.println("hello, guy");
    }

因此,sr.sayHello(man);实际调用的方法就是上面的public void sayHello(Human guy)方法。

同样地,sr.sayHello(woman);的方法接收者的实际类型是StaticDispatch对象sr,由序号31可知方法常量还是Method sayHello:(Lorg/hapjin/dynamic/StaticDispatch$Human;)V ,因此,实际调用的方法还是public void sayHello(Human guy)

从这里可看出:对于重载(Overload)而言,它的方法接收者的类型是相同的,那调用哪个重载方法就取决于:传入的参数类型、参数的数量等。而参数类型在编译器生成字节码的时候就已经确定了,比如上面的sayHello方法的参数类型都是Human(sayHello:(Lorg/hapjin/dynamic/StaticDispatch$Human;)V

因此,sr.sayHello(man);sr.sayHello(woman);执行的是相同的方法public void sayHello(Human guy){}

接下来看看:覆盖(Override),也即运行时多态的执行情况:

javap -verbose DynamicDispatch

上面截取的是DynamicDispatch.java的main方法的执行过程。从序号17和21 可知:man.sayHello();woman.sayHello();也都是由虚拟机指令invokevirtual指令执行的,并且调用的sayHello方法的符号引用都是Method org/hapjin/dynamic/DynamicDispatch$Human.sayHello:()V

那为什么最终执行的结果却是:man.sayHello()输出 ‘man say hello‘,而woman.sayHello()输出‘woman say hello‘呢?

    man.sayHello();//man say hello
    woman.sayHello();//woman say hello

下面再来过一遍invokevirtual指令的执行过程。当虚拟机执行到man.sayHello()这条语句时,invokevirtual指令第一步:找到操作数栈顶的第一个元素,这个元素就是序号7 astore_1存进去的,它是一个Man类型的对象

接下来,第二步,在 Man 类中寻找与常量中描述符和简单名称都相符的方法,在这里常量是Method org/hapjin/dynamic/DynamicDispatch$Human.sayHello:()V,而Man 类中与该常量的描述符和简单名称都相符的方法,显然就是 Man 类中的sayHello方法了。

于是invokevirtual指令就把 常量池中的类方法符号引用 解析 到了 具体的Man类的sayHello方法的直接引用上。

同理,类似地,在执行woman.sayHello()这条语句时,invokevirtual指令找到的操作数栈顶的第一个元素是由 指令15astore_2存储进去的Woman类型的对象。于是,在Woman类中 寻找与常量池类方法的符号引用Method org/hapjin/dynamic/DynamicDispatch$Human.sayHello:()V都相符的方法,这个方法就是Woman类中的sayHello方法。

从上面的invokevirtual指令的执行过程看,语句man.sayHello();woman.sayHello(); 对应的类方法的符号引用是一样的,都是org/hapjin/dynamic/DynamicDispatch$Human.sayHello:()V,但由于方法接受者的实际类型不同,一个是Man类型、另一个是Woman类型,因为最终执行的方法也就不一样了。

文中涉及到的一些额外的概念:

  • 方法接收者:sr.sayHello(new Man()), sr 对象就是 sayHello方法的接收者
  • 常量:常量池中的常量,可参考常量池中的项目类型
  • 描述符:用来描述字段的数据类型,方法的参数列表和返回值,方法的参数列表指的是:方法有多少个参数、方法的参数是什么类型、参数的顺序
  • 简单名称:没有类型和参数修饰的方法或者字段名称。比如说方法:public void m(String a){},那简单名称就是 m

以上纯个人理解,有些概念可能表述地不太严谨,若有错误,望指正,感激不尽。

写完这篇文章,我抬头望向窗外,天又黑了。目光缓缓移回到电脑屏幕上,一个技术人的追求到底是什么?我应该往哪个方向深入下去呢?后台、算法、ML、或者高大上的DL?

于是又想起了上一次的对话中那个人说的:关键是看你能不能持续地花时间把背后的原理搞清楚。

参考书籍:《深入理解JVM虚拟机》

原文:https://www.cnblogs.com/hapjin/p/9248525.html

原文地址:https://www.cnblogs.com/hapjin/p/9248525.html

时间: 2024-08-09 10:44:30

从虚拟机指令执行的角度分析JAVA中多态的实现原理的相关文章

Java中多态的一些简单理解

什么是多态 1.面向对象的三大特性:封装.继承.多态.从一定角度来看,封装和继承几乎都是为多态而准备的.这是我们最后一个概念,也是最重要的知识点. 2.多态的定义:指允许不同类的对象对同一消息做出响应.即同一消息可以根据发送对象的不同而采用多种不同的行为方式.(发送消息就是函数调用) 3.实现多态的技术称为:动态绑定(dynamic binding),是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法. 4.多态的作用:消除类型之间的耦合关系. 5.现实中,关于多态的例子不

个人对Java中多态的一些简单理解

什么是多态 面向对象的三大特性:封装.继承.多态.从一定角度来看,封装和继承几乎都是为多态而准备的.这是我们最后一个概念,也是最重要的知识点. 多态的定义:指允许不同类的对象对同一消息做出响应.即同一消息可以根据发送对象的不同而采用多种不同的行为方式.(发送消息就是函数调用) 实现多态的技术称为:动态绑定(dynamic binding),是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法. 多态的作用:消除类型之间的耦合关系. 现实中,关于多态的例子不胜枚举. 下面是多态

Java中CAS底层实现原理分析

CAS(无锁优化.自旋锁)原理分析 一.CAS(compareAndSwap)的概念 CAS,全称Compare And Swap(比较与交换),解决多线程并行情况下使用锁造成性能损耗的一种机制. CAS(V, A, B),V为内存地址.A为预期原值,B为新值.如果内存地址的值与预期原值相匹配,那么将该位置值更新为新值.否则,说明已经被其他线程更新,处理器不做任何操作:无论哪种情况,它都会在 CAS 指令之前返回该位置的值.而我们可以使用自旋锁,循环CAS,重新读取该变量再尝试再次修改该变量,也

通过字节码分析java中的switch语句

在一次做题中遇到了switch的问题,由于对switch执行顺序的不了解,在这里简单的通过字节码的方式理解一下switch执行顺序(题目如下): public class Ag{ static public int i=10; public static void main(String []args){ switch(i){ default: System.out.println("this is default"); case 1: System.out.println("

深入源码分析Java线程池的实现原理

程序的运行,其本质上,是对系统资源(CPU.内存.磁盘.网络等等)的使用.如何高效的使用这些资源是我们编程优化演进的一个方向.今天说的线程池就是一种对CPU利用的优化手段. 网上有不少介绍如何使用线程池的文章,那我想说点什么呢?我希望通过学习线程池原理,明白所有池化技术的基本设计思路.遇到其他相似问题可以解决. 池化技术 前面提到一个名词--池化技术,那么到底什么是池化技术呢? 池化技术简单点来说,就是提前保存大量的资源,以备不时之需.在机器资源有限的情况下,使用池化技术可以大大的提高资源的利用

通过基于java实现的网络聊天程序分析java中网络API和Linux Socket API关系

1. 引言 socket网络编程,可以指定不同的通信协议,在这里,我们使用TCP协议实现基于java的C/S模式下“hello/hi”网络聊天程序 2. 目标 1). 通过该网络聊天程序,了解java Socket API接口的基本用法 2). java Socket API简要介绍 3). linux socket API 简单分析 4). tcp协议的连接和终止 5). 探究java socket API 和 linux socket api之间的关系 3. linux socket API

就是要你懂Java中volatile关键字实现原理

原文地址http://www.cnblogs.com/xrq730/p/7048693.html,转载请注明出处,谢谢 前言 我们知道volatile关键字的作用是保证变量在多线程之间的可见性,它是java.util.concurrent包的核心,没有volatile就没有这么多的并发类给我们使用. 本文详细解读一下volatile关键字如何保证变量在多线程之间的可见性,在此之前,有必要讲解一下CPU缓存的相关知识,掌握这部分知识一定会让我们更好地理解volatile的原理,从而更好.更正确地地

Java中volatile关键字实现原理

原文地址http://www.cnblogs.com/xrq730/p/7048693.html,转载请注明出处,谢谢 前言 我们知道volatile关键字的作用是保证变量在多线程之间的可见性,它是java.util.concurrent包的核心,没有volatile就没有这么多的并发类给我们使用. 本文详细解读一下volatile关键字如何保证变量在多线程之间的可见性,在此之前,有必要讲解一下CPU缓存的相关知识,掌握这部分知识一定会让我们更好地理解volatile的原理,从而更好.更正确地地

深入Java核心 Java中多态的实现机制(1)

多态性是Java面向对象的一个重要机制,本文将向您详细介绍Java语言中多态性的实现原理和方法,通过多态一点带出更多Java面向对象有趣而实用的知识. 多态性是面向对象程序设计代码重用的一个重要机制,我们曾不只一次的提到Java多态性.在Java运行时多态性:继承和接口的实现一文中,我们曾详细介绍了Java实现运行时多态性的动态方法调度:今天我们再次深入Java核心,一起学习Java中多态性的实现. “polymorphism(多态)”一词来自希腊语,意为“多种形式”.多数Java程序员把多态看