方法调用过程是指确定被调用方法的版本(即调用哪一个方法),并不包括方法执行过程。我们知道,Class 文件的编译过程中并不包括传统编译中的连接步骤,一切方法调用在 Class 文件调用里面存储的都只是符号引用,而不是方法在实际运行时的内存布局入口地址,也就是说符号引用解析成直接引用的过程。这个特性使得Java 具有强大的动态扩展能力,但也使得 Java方法调用过程变得复杂起来,需要在类加载器件,甚至是运行期间才确定目标方法的直接饮用。

1、解析调用

在类加载的解析阶段,会将其中一部分符号引用直接转化为直接饮用,前提是:方法在程序真正运行之前就有一个可确定的版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好,编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution)。

符合这个要求的方法,主要包括:静态方法 和 私有方法。因为这两个方法的特点就决定了他们都不可能通过继承或别的方式重写其他版本。

与之相对应的,Java 虚拟机里面提供了5条方法调用字节码指令,分别如下:

  • invokestatic:调用静态方法
  • invokespecial:调用<init>方法、私有方法和父类方法
  • invokevirtual:调用所有的虚方法
  • invokeinterface:调用接口方法,会在运行时在确定一个实现此接口的对象
  • invokedynamic:会在运行时动态解析出调用电限定符所引用的方法,然后再执行该方法。
只能被 invokestatic 和 invokespecial 调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法4类,这些方法称为非虚方法,由于 final 修饰的方法不能被覆盖,也属于非虚方法。与之相反,其他的方法称为虚方法。

2、分派调用

作为一门面向对象的程序语言,Java 具备面型对象的3个特征:继承、封装和多态。下面我们将会讲解多态性特征的一些最基本的体现,如“重写”和“重载”在Java 虚拟机中是怎么实现的。

2.1静态分派

依赖于静态类型来定位方法执行版本的分派动作(如重载)称为静态分派。

首先我们先来看一下下面的这段代码:
  1. package org.fenixsoft.polymorphic;
  2. /**
  3. * 方法静态分派演示
  4. * @author zzm
  5. */
  6. public class StaticDispatch {
  7. static abstract class Human {
  8. }
  9. static class Man extends Human {
  10. }
  11. static class Woman extends Human {
  12. }
  13. public void sayHello(Human guy) {
  14. System.out.println("hello,guy!");
  15. }
  16. public void sayHello(Man guy) {
  17. System.out.println("hello,gentleman!");
  18. }
  19. public void sayHello(Woman guy) {
  20. System.out.println("hello,lady!");
  21. }
  22. public static void main(String[] args) {
  23. Human man = new Man();
  24. Human woman = new Woman();
  25. StaticDispatch sr = new StaticDispatch();
  26. sr.sayHello(man);
  27. sr.sayHello(woman);
  28. }
  29. }

运行结果如下:

hello,guy!
hello,guy!
为什么会选择参数类型为 Human 的重载呢?首先,我们看下面两个重要概念。
        Human man = new Man();
我们将上面代码中的 “Human” 称为变量”man“的 静态类型(Static Type),或者叫做 外观类型(Apparent Type)。后面的 ”Man“则称为变量的实际类型(Actual Type)。
静态类型和实际类型都可以发生变化,区别是静态类型仅仅在使用时变化,变量本身的静态类型不会改变,并且最终的静态类型是编译可知的。而实际类型变化结果只有在运行期才确定,编译器在编译程序的时候,不知道一个对象的实际类型是什么。
虚拟机在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译器可知的,因此,在编译期,Javac 编译器会根据参数的静态类型决定使用哪个重载版本,所以选择了 sayHello(Human),并把这个方法的符号引用写入 main() 方法的两条 invokevirtual 指令的参数中。

2.2 动态分派

运行时期,依赖于实际类型来定位方法执行的分派动作(重写 Override) 属于动态分派。
  1. public class DynamicDispatch {
  2. static abstract class Human {
  3. protected abstract void sayHello();
  4. }
  5. static class Man extends Human {
  6. @Override
  7. protected void sayHello() {
  8. System.out.println("man say hello");
  9. }
  10. }
  11. static class Woman extends Human {
  12. @Override
  13. protected void sayHello() {
  14. System.out.println("woman say hello");
  15. }
  16. }
  17. public static void main(String[] args) {
  18. Human man = new Man();
  19. Human woman = new Woman();
  20. man.sayHello();
  21. woman.sayHello();
  22. man = new Woman();
  23. man.sayHello();
  24. }
  25. }

运行结果:

man say hello
woman say hello
woman say hello
我们用 javap命令输出这段代码的字节码:
  1. public static void main(java.lang.String[]);
  2. flags: ACC_PUBLIC, ACC_STATIC
  3. Code:
  4. stack=2, locals=3, args_size=1
  5. 0: new           #16                 // class org/bupt/xiaoye/DynamicDispatch$Man
  6. 3: dup
  7. 4: invokespecial #18                 // Method org/bupt/xiaoye/DynamicDispatch$Man."<init>":()V
  8. 7: astore_1
  9. 8: new           #19                 // class org/bupt/xiaoye/DynamicDispatch$Woman
  10. 11: dup
  11. 12: invokespecial #21                 // Method org/bupt/xiaoye/DynamicDispatch$Woman."<init>":()V
  12. 15: astore_2
  13. 16: aload_1
  14. 17: invokevirtual #22                 // Method org/bupt/xiaoye/DynamicDispatch$Human.sayHello:()V
  15. 20: aload_2
  16. 21: invokevirtual #22                 // Method org/bupt/xiaoye/DynamicDispatch$Human.sayHello:()V
  17. 24: new           #19                 // class org/bupt/xiaoye/DynamicDispatch$Woman
  18. 27: dup
  19. 28: invokespecial #21                 // Method org/bupt/xiaoye/DynamicDispatch$Woman."<init>":()V
  20. 31: astore_1
  21. 32: aload_1
  22. 33: invokevirtual #22                 // Method org/bupt/xiaoye/DynamicDispatch$Human.sayHello:()V
  23. 36: return
  24. LineNumberTable:
  25. line 28: 0
  26. line 29: 8
  27. line 30: 16
  28. line 31: 20
  29. line 32: 24
  30. line 33: 32
  31. line 34: 36
  32. LocalVariableTable:
  33. Start  Length  Slot  Name   Signature
  34. 0      37     0  args   [Ljava/lang/String;
  35. 8      29     1   man   Lorg/bupt/xiaoye/DynamicDispatch$Human;
  36. 16      21     2 woman   Lorg/bupt/xiaoye/DynamicDispatch$Human;
  37. }

接下来的16-21句是关键部分。16、20两句将创建的两个对象的引用压到栈顶,这两个对象是将要执行的 sayHello() 方法的所有者,称为接受者(Receiver);17、21句是方法调用指令,但是这两条指令的最终执行的目标方法并不相同。原因就需要从 invokevirtual 指令的动态查找过程开始说起, invokevirtual 指令的运行时解析过程大致分为以下几个步骤:

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

2.3 单分派与多分派

方法的接受者与方法的参数统称为方法的宗量。根据分派基于多少宗量,可以将分派划分为单分派和多分派两种。
我们来看一段代码
  1. public class Dispatch {
  2. static class QQ {}
  3. static class _360 {}
  4. public static class Father {
  5. public void hardChoice(QQ arg) {
  6. System.out.println("father choose qq");
  7. }
  8. public void hardChoice(_360 arg) {
  9. System.out.println("father choose 360");
  10. }
  11. }
  12. public static class Son extends Father {
  13. public void hardChoice(QQ arg) {
  14. System.out.println("son choose qq");
  15. }
  16. public void hardChoice(_360 arg) {
  17. System.out.println("son choose 360");
  18. }
  19. }
  20. public static void main(String[] args) {
  21. Father father = new Father();
  22. Father son = new Son();
  23. father.hardChoice(new _360());
  24. son.hardChoice(new QQ());
  25. }
  26. }
单分派是根据一个宗量对目标方法进行选择,多分派则是根据多余一个宗量对目标方法进行选择。
在静态分派的过程中,选择目标方法的依据有两点:1、看对象的静态类型时什么,即使 Father 还是 Son。 2、方法参数的类型和数量是什么是 QQ还是 360 。因为是根据两个宗量进行选择,所以 Java 语言的静态分派属于多分派类型。
在动态分派的过程中,由于编译器已经决定了目标方法的签名,因此只需要找到方法的接受者就可以了。因为是根据一个宗量进行选择,所以 Java 语言的动态分派属单分派类型。
 

2.4 动态分配的实现

由于动态分配是非常频繁的动作,而且动态分配的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因此在虚拟机的实际实现中,基于性能的考虑,大部分实现都不会真正的进行如此频繁的搜索。最常用的手段就是为类在方法去中建立一个虚方法表(Virtual Method Table , 也称为 vtable ,与此对应的,在 invokeinterface 执行时也会用到接口方法表-Inteface Method Table , 简称 itable),使用虚方法表索引来代替元数据查找以提高性能。
以上面代码为例,虚方法表结构如图:
 
 
虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和弗雷相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口。如上图,Son 重写了来自于 Father 的全部方法,因此 Son 的方发表没有指向 Father 类型数据的箭头。但是 Son 和 Father 都没有重写来自于 Object 的方法,所以他们的方法表中所有从 Object 继承来的方法都指向了 Object 的数据类型。
方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值之后,虚拟机会把该类的方法表也初始化完毕。

深入理解java虚拟机(十一) 方法调用-解析调用与分派调用的更多相关文章

  1. 《深入理解 java虚拟机》学习笔记

    java内存区域详解 以下内容参考自<深入理解 java虚拟机 JVM高级特性与最佳实践>,其中图片大多取自网络与本书,以供学习和参考.

  2. 深入理解java虚拟机(5)---字节码执行引擎

    字节码是什么东西? 以下是百度的解释: 字节码(Byte-code)是一种包含执行程序.由一序列 op 代码/数据对组成的二进制文件.字节码是一种中间码,它比机器码更抽象. 它经常被看作是包含一个执行 ...

  3. 深入理解java虚拟机(4)---类加载机制

    类加载的过程包括: 加载class到内存,数据校验,转换和解析,初始化,使用using和卸载unloading过程. 除了解析阶段,其他过程的顺序是固定的.解析可以放在初始化之后,目的就是为了支持动态 ...

  4. 深入理解java虚拟机系列(一):java内存区域与内存溢出异常

    文章主要是阅读<深入理解java虚拟机:JVM高级特性与最佳实践>第二章:Java内存区域与内存溢出异常 的一些笔记以及概括. 好了開始.假设有什么错误或者遗漏,欢迎指出. 一.概述 先上 ...

  5. 深入理解Java虚拟机--下

    深入理解Java虚拟机--下 参考:https://www.zybuluo.com/jewes/note/57352 第10章 早期(编译期)优化 10.1 概述 Java语言的"编译期&q ...

  6. 深入理解Java虚拟机--中

    深入理解Java虚拟机--中 第6章 类文件结构 6.2 无关性的基石 无关性的基石:有许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码(ByteCode),从而 ...

  7. 深入理解Java虚拟机--上

    深入理解Java虚拟机--上 第2章 Java内存区域和内存溢出异常 2.2 运行时数据区域 图 2-1 Java虚拟机运行时数据区 2.2.1 程序计数器 程序计数器可以看作是当前线程所执行的字节码 ...

  8. 深入理解java虚拟机之java内存区域

    java虚拟机在执行java程序的时候会把它所管理的内存分为多个不同的区域,每个区域都有不同的作用,以及由各自的生命周期,有些随着虚拟机进行的启动而存在,有些区域则依赖于用户线程的启动或结束而建立或销 ...

  9. 运用《深入理解Java虚拟机》书中知识解决实际问题

    前言 以前看别人博客说看完<深入理解Java虚拟机>这本书并没有让自己的编程水平提高多少,不过却大大提高了自己的装逼水平.其实,我倒不这么认为,至少在我看完一遍这本书后,有一种醍醐灌顶的感 ...

  10. 《深入理解Java虚拟机》-----第8章 虚拟机字节码执行引擎——Java高级开发必须懂的

    概述 执行引擎是Java虚拟机最核心的组成部分之一.“虚拟机”是一个相对于“物理机”的概念 ,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器.硬件.指令集和操作系统层面上的,而 ...

随机推荐

  1. 在centos7上搭建mongodb副本集

    1.安装副本集介绍 副本集(Replica Set)是一组MongoDB实例组成的集群,由一个主(Primary)服务器和多个备份(Secondary)服务器构成.通过Replication,将数据的 ...

  2. lunix,命令集锦

    1. ls命令 ls命令是列出目录内容(List Directory Contents)的意思.运行它就是列出文件夹里的内容,可能是文件也可能是文件夹. ? 1 2 3 4 5 6 7 root@te ...

  3. MySQL多实例介绍

    我们前面已经做了MySQL数据库的介绍以及为什么选择MySQL数据库,最后介绍了MySQL数据库在Linux系统下的多种安装方式,以及讲解了MySQL的二进制方式单实例安装.基础优化等,下面给大家讲解 ...

  4. [Z]图灵奖获得者Richard Karp讲述Berkeley CS的发展史

    A Personal View of Computer Science at Berkeley 赤裸裸的吊炸天

  5. C# 设计模式-工厂模式(Factory)

    1.工厂模式 factory从若干个可能类创建对象. 例如:如果创建一个通信类接口,并有多种实现方式,可以使用factory创建一个实现该接口的对象,factory可以根据我们的选择,来创建适合的对象 ...

  6. 免费视频教学:30天精通iPho…

    原文地址:免费视频教学:30天精通iPhone手机编程(全)作者:苹果iphone软件编程 土豆连接http://www.tudou.com/playlist/id12638619.html

  7. centos7 时间修改

    转子 http://blog.csdn.net/kuluzs/article/details/52825331 在CentOS 6版本,时间设置有date.hwclock命令,从CentOS 7开始, ...

  8. 压力测试工具--Siege

    Siege是一款开源的压力测试工具,设计用于评估WEB应用在压力下的承受能力.可以根据配置对一个WEB站点进行多用户的并发访问,记录每个用户所有请求过程的相应时间,并在一定数量的并发访问下重复进行.s ...

  9. 【LA2957 训练指南】运送超级计算机【二分,最大流】

    题意: 宇宙中有n个星球,你的任务是用最短的时间把k个超级计算机从星球S运送到星球T.每个超级计算机需要一整艘飞船来运输.行星之间有m条双向隧道,每条隧道需要一整天的时间来通过,且不能有两艘飞船同时使 ...

  10. 高性能Web服务器Nginx的配置与部署研究(9)核心模块之HTTP模块基本常用指令

    一.HTTP模块的作用是什么? Nginx的HTTP模块用于控制Nginx的HTTP进程. 二.指令 1. alias 含义:指定location使用的路径,与root类似,但不改变文件的跟路径,仅适 ...