我们知道面向对象语言的三大特点之一就是多态性,而java作为一种面向对象的语言,自然也满足多态性,我们也知道java中的多态包括重载与重写,我们也知道在C++中动态多态是通过虚函数来实现的,而虚函数是通过一个虚函数表来完成的,这也很好理解,那么java语言的多态性是怎么实现的呢?在java中是否也存在类似C++中的虚函数表的结构呢?这就需要我们从java虚拟机字节码执行引擎的执行过程来找答案了,下面就从java虚拟机字节码执行引擎的执行过程带领大家彻底理解java中的多态性。

通过前面的【java虚拟机系列】java虚拟机系列之JVM总述,我们知道java的运行时数据区中包含一个叫做java栈的区域,该区域的作用就是用来描述java方法调用的执行模型。

我们也知道在每个方法执行时会创建一个叫做栈帧的数据结构用来描述当前方法的一些信息。这些信息主要包括方法的局部变量表,操作数栈,动态链接和方法的返回地址等信息。用图表示如下:

其中的动态连接部分就是用来支持多态性而存在的,我们知道java中的Class文件的常量池中存在大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转换称为静态解析,另一种将在每次运行时转换为直接引用,这种称为动态连接。下面详细讲解这两种情况。

一方法的解析调用

前面讲过java中的方法调用的目标方法在Class文件中都只是一个常量池中的符号引用,在类加载的解析阶段(关于类加载的知识请参看我的博客:http://blog.csdn.net/htq__/article/details/50990939),会将一部分符号引用转换为直接引用(即最终调用的方法的信息字段),前提条件是在程序运行之前就能够确定调用那个版本的方法,且在运行期间不可更改,满足这两个条件的方法主要包括:静态方法和私有方法。因为静态方法属于整个类的,与类型直接相关联,在申明的时候是哪个类则调用的是哪个类的静态方法,而私有方法对外部不可访问,因此不可能存在重写的其它版本,因此这两种方法在类的加载阶段就进行解析。下面这段代码很好的说明了这点。

class Base{
static void fun(){System.out.println("base"); }
}
}
public class Inherit extends Base{ static void fun(){//注意此时子类不是重写基类的fun函数,而是隐藏了基类的fun函数
//因为被static修饰
System.out.println("inherit");
}
}
public static void main(String args[]){
Base x=new Base();
Base y=new Inherit();
x.fun(); //因为fun函数被static修饰,所以在类的加载阶段就完成解析
y.fun();
}
}

程序的运行结果为:

base

base

程序的运行结果也说明了static方法在类的加载阶段就完成了解析,因为x与y的申明类型都为Base类型,所以调用的fun函数都为Base的static的fun函数。

解析调用在编译期完全确定,在类装载的解析阶段就会把相关的符号引用全部转换为可确定的直接引用,不会延迟到运行期再去确定。

另外final方法也是唯一确定的,这个很好理解因为final表示不可以被重写,因此只可能存在一个版本。

二方法的分派调用

分派调用包括静态分派与动态分派,下面一一讲解。

  1静态分派(重载)

首先我们来看一段代码,读者可以先预测一下这段代码的输出结果。

public class StaticDispatch {

	static abstract class People {
} static class Boy extends People {
} static class Girl extends People {
} public void sayHello(People guy) {
System.out.println("hello,guy!");
} public void sayHello(Boy guy) {
System.out.println("hello,boy!");
} public void sayHello(Girl guy) {
System.out.println("hello,girl!");
} public static void main(String[] args) {
People boy = new boy();//boy为子类的上转型对象
People girl = new Girl();//girl为子类的上转型对象
StaticDispatch sr = new StaticDispatch();
sr.sayHello(boy);
sr.sayHello(girl);
}
}

输出结果为:

hello,guy!

hello,guy!

输出结果在我们的预料之中,那么为何输出结果是这个呢?或者说java中的重载调用的规则是怎样的呢?

首先我们解释两个概念:外观类型(Apparent Type)与实际类型(Actual Type),这两个概念在上转型对象中经常用到,如People boy = new Boy();//boy为子类的上转型对象。我们把People叫做引用boy的外观类型,而Boy则是实际类型。这两者之间的不同之处就是外观类型在编译期就已经可知,而实际类型在运行期间在可以确定,编译器在编译期间可能不知道一个对象的实际类型是啥。



之所以输出结果如上所示,是因为虚拟机(更确切的说是编译器)在重载时是通过参数的外观类型而不是实际类型来作为重载调用的判定依据的,外观类型是在编译期可知的,因此,在编译阶段,javac编译去会根据参数的外观类型来确定调用哪个重载版本,所以最终都是选择的sayHello(People guy) 这个重载版本。







2动态分派(重写)

首先解释一下java中的动态多态性:让父类的引用指向不同的子类对象时(即上转型对象),上转型对象调用方法实际调用的是子类重写的方法。我们仍然以上述代码为例进行讲解。

public class DynamicDispatch {

	static abstract class People {
protected abstract void sayHello();
} static class Boy extends People {
@Override
protected void sayHello() {
System.out.println("boy say hello");
}
} static class Girl extends People {
@Override
protected void sayHello() {
System.out.println("girl say hello");
}
} public static void main(String[] args) {
People boy = new Boy();//boy为Boy的上转型对象
People girl = new Girl();//girl为Girl的上转型对象
boy.sayHello();
girl.sayHello();
boy = new Girl();//更改boy的实际类型,让其指向Girl对象
boy.sayHello();
}
}

程序输出结果为:

boy say hello

girl say hello

girl say hello

同样输出结果在我们的预料之中,那么为何输出结果是这个呢?或者说java中的重写调用的规则是怎样的呢?

这就涉及到java字节码执行引擎中的invokevirtual指令,invokevirtual指令的多态性查找过程如下所示:

1找到操作数栈顶的第一个元素(即调用方法的外观类型)所指向对象的实际类型,即为T。

2如果在类型T中找到了与常量中的描述符和简单名称都相符的方法(即子类重写了父类的方法),则进行访问权限检验,如果通过则返回这个方法的直接引用(即将会调用子类中重写的方法),查找过程结束,如果不通过,则抛出java.lang.IllegalAccessError异常。

3如果在步骤2中没找到(即子类的实际类型没重写父类的方法),则按照继承关系从下往上对T的各个父类进行第2步的搜索和验证过程。

4如果始终没找到合适的方法(即实际类型T和其间接父类都没重写其超基类的方法),则会抛出java.lang.AbstractMethodError异常。

从上述过程可以看到,虽然压入操作数栈的仍然是外观类型,但是在调用重写方法时invokevirtual指令会在运行期间确定调用者的实际类型,所以两次调用invokevirtual指令会把常量池中的类方法的符号引用解析到了不同的直接引用上(一次为Boy一次为Girl),这样调用到的就是子类重写的父类的方法。

三虚拟机是如何实现的动态分派



在开头我们讲过在C++中动态调用是通过虚函数表来实现的,其实在java中也是这么做的,因为动态分派是非常频繁的操作,且动态分派方法的选择需要在程序运行期间动态的确定,因此虚拟机在实现时基于性能的考虑,不会像上述介绍的那样在运行时去搜索应该调用那个重写版本,而是在类的方法区建立一个虚方法表(Virtual Method Table 简称vtable,与此对应的实现接口重写的方法会用到接口方法表(Interface Method Table 简称itable))i,使用虚方法表的索引来代替元数据的查找从而提高性能。那么虚方法表中存储哪些数据呢?

虚方法表中存放着各个方法的实际调用的入口地址。如果某个方法在子类中没被重写,那么子类的虚方法表中地址入口与父类相同方法的入口地址是同一个地址,如果子类重写了父类中的方法,那么子类的虚方法表中地址将会替换为指向子类实现的重写的函数的入口地址。

通常具备相同签名的方法,在父类与子类的虚方法表中具备相同的索引号。方法表一般在类加载的连接阶段进行初始化,在准备了类的变量初始值之后,虚拟机会把类的虚方法表初始化完毕。

以上就是本博客的主要内容,如果读者觉得不错,记得小手一抖,点个赞哦!另外欢迎大家关注我的博客账号哦,将会不定期的为大家分享技术干货,福利多多哦!



【java虚拟机系列】从java虚拟机字节码执行引擎的执行过程来彻底理解java的多态性的更多相关文章

  1. Java安全之动态加载字节码

    Java字节码 简单说,Java字节码就是.class后缀的文件,里面存放Java虚拟机执行的指令. 由于Java是一门跨平台的编译型语言,所以可以适用于不同平台,不同CPU的计算机,开发者只需要将自 ...

  2. 《深入理解Java虚拟机》学习笔记之字节码执行引擎

    Java虚拟机的执行引擎不管是解释执行还是编译执行,根据概念模型都具有统一的外观:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果. 运行时栈帧结构 栈帧(Stack Frame) ...

  3. java动态代理——字段和方法字节码的基础结构及Proxy源码分析三

    前文地址:https://www.cnblogs.com/tera/p/13280547.html 本系列文章主要是博主在学习spring aop的过程中了解到其使用了java动态代理,本着究根问底的 ...

  4. Java 集合系列Stack详细介绍(源码解析)和使用示例

    Stack简介 Stack是栈.它的特性是:先进后出(FILO, First In Last Out). java工具包中的Stack是继承于Vector(矢量队列)的,由于Vector是通过数组实现 ...

  5. Java Eclipse编译后产生的字节码文件,用DOS命令符怎么打开

    在很多初学者刚刚接触eclipse的时候,写完一个代码文件.例如 Demo.java 通过run as a java application生成之后,会产生一个Demo.class. Demo.cla ...

  6. 8.5(java学习笔记)8.5 字节码操作(javassist)

    一.javassist javassist让我们操作字节码更加简单,它是一个类库,允许我们修改字节码.它允许java程序动态的创建.修改类. javassist提供了两个层次的API,基于源码级别的和 ...

  7. jvm系列四类加载与字节码技术

    四.类加载与字节码技术 1.类文件结构 首先获得.class字节码文件 方法: 在文本文档里写入java代码(文件名与类名一致),将文件类型改为.java java终端中,执行javac X:...\ ...

  8. [Java并发] AQS抽象队列同步器源码解析--独占锁释放过程

    [Java并发] AQS抽象队列同步器源码解析--独占锁获取过程 上一篇已经讲解了AQS独占锁的获取过程,接下来就是对AQS独占锁的释放过程进行详细的分析说明,废话不多说,直接进入正文... 锁释放入 ...

  9. JVM执行引擎总结(读《深入理解JVM》) 早期编译优化 DCE for java

    execution engine: 运行时栈current stack frame主要保存了 local variable table, operand stack, dynamic linking, ...

随机推荐

  1. ●UVa 11346 Probability

    题链: https://vjudge.net/problem/UVA-11346题解: 连续概率,积分 由于对称性,我们只用考虑第一象限即可. 如果要使得面积大于S,即xy>S, 那么可以选取的 ...

  2. ●poj 1474 Video Surveillance

    题链: http://poj.org/problem?id=1474 题解: 计算几何,半平面交 半平面交裸题,快要恶心死我啦... (了无数次之后,一怒之下把onleft改为onright,然后还加 ...

  3. UVA - 11997:K Smallest Sums

    多路归并 #include<cstdio> #include<cstdlib> #include<algorithm> #include<cstring> ...

  4. 洛谷mNOIP模拟赛Day1-数颜色

    传送门 题目大意: 给定一个序列,维护每个数字在[L,R]出现的次数以及交换a[x]和a[x+1]的操作 一开始想的分桶法,感觉复杂度还可以吧,常数有点大,于是死得很惨(65分) #include&l ...

  5. poj3270 && poj 1026(置换问题)

    | 1 2 3 4 5 6 | | 3 6 5 1 4 2 | 在一个置换下,x1->x2,x2->x3,...,xn->x1, 每一个置换都可以唯一的分解为若干个不交的循环 如上面 ...

  6. [BZOJ]1089 严格n元树(SCOI2003)

    十几年前的题啊……果然还处于高精度遍地走的年代.不过通过这道题,小C想mark一下n叉树计数的做法. Description 如果一棵树的所有非叶节点都恰好有n个儿子,那么我们称它为严格n元树.如果该 ...

  7. Python3 运算符

    装载自:https://www.cnblogs.com/cisum/p/8064222.html Python3 运算符 什么是运算符? 本章节主要说明Python的运算符.举个简单的例子 4 +5 ...

  8. Python 线程池,进程池,协程,和其他

    本节内容 线程池 进程池 协程 try异常处理 IO多路复用 线程的继承调用 1.线程池 线程池帮助你来管理线程,不再需要每个任务都创建一个线程进行处理任务. 任务需要执行时,会从线程池申请线程,有则 ...

  9. B/S与C/S架构

    1.CS.BS架构定义 CS(Client/Server):客户端----服务器结构.C/S结构在技术上很成熟,它的主要特点是交互性强.具有安全的存取模式.网络通信量低.响应速度快.利于处理大量数据. ...

  10. Python中dict的功能介绍

    Dict的功能介绍 1. 字典的两种函数(方法) 1. 字典的内置函数 包含关系 格式:x.__contains__(key)等同于key in x 例如:dic = {'ab':23,'cd':34 ...