【深入理解JVM】:解析与分派

原创 2016年05月06日 20:56:50

解析

Java中方法调用的目标方法在Class文件里面都是常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用。(关于符号引用与直接引用,详见【深入理解JVM】:Class类文件结构)这种解析的前提是:方法在程序真正运行之前就有一个可以确定的调用版本,并且这个方法的调用版本在运行期是不可改变的,即“编译期可知,运行期不可变”,这类目标的方法的调用称为解析(Resolve)

只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合条件的有静态方法(invokestatic指令)、私有方法、实例构造方法、父类方法(这3个是invokespecial指令),它们在类加载的的解析阶段就会将符号引用解析为该方法的直接引用。

关于解析的具体过程详见【深入理解JVM】:类加载机制

分派

解析调用一定是个静态的过程,在编译期就完全确定,在类加载的解析阶段就将涉及的符号引用全部转变为可以确定的直接引用,不会延迟到运行期再去完成。而分派(Dispatch)调用则可能是静态的也可能是动态的。分派是多态性的体现,Java虚拟机底层提供了我们开发中“重载”(Overload)“和重写”(Override)的底层实现。其中重载属于静态分派,而重写则是动态分派的过程。

静态分派

静态分派只会涉及重载(Oveload),而重载是在编译期间确定的,那么静态分派自然是一个静态的过程(因为还没有涉及到Java虚拟机)。静态分派的最直接的解释是在重载的时候是通过参数的静态类型而不是实际类型作为判断依据的。因此在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本。

下面的代码演示了静态分派(重载)的过程:

public class StaticDispatch {

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch sr = new StaticDispatch();
        sr.sayHello(man);
        sr.sayHello(woman);
    }

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

输出结果为:

hello, guy
hello, guy

首先弄清楚什么是静态类型和实际类型,在语句Human man = new Man();中,Human称为变量的静态类型(Static Type)或者外观类型(Apparent Type),Man称为变量的实际类型(Actual Type)。静态类型和实际类型在程序中都可以发生变化,但是静态类型的变化仅仅发生在使用时,变量本身的静态类型不会改变,最终的静态类型在编译期是可知的;而实际类型变化的结果在运行期才可以确定,编译时并不知道一个对象的实际类型是什么。例如:

// 实际类型变化
Human man = new Man();
man = new Woman();
// 静态类型变化
sr.sayHello((Man) man);
sr.sayHello((Woman) man);

在该实例代码中,main()方法的两次调用sayHello(),在方法接收者已经确定是对象sr的前提下,使用哪个重载版本就完全取决于传入参数的数量和数据类型。代码中使用了两个静态类型相同而实际类型不同的变量,但是Javac编译期在重载时是通过参数的静态类型而不是实际类型作为判定依据的,man和woman的静态类型都是Human。静态类型在编译期可知,因此在编译阶段,编译期根据man和woman的静态类型为Human的事实,选择sayHello(Human)作为调用目标,这就是方法重载的本质

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派是还没有涉及到虚拟机,由编译期执行。虽然编译器能够在编译阶段确定方法的版本,但是很多情况下重载的版本不是唯一的,在这种模糊的情况下,编译器会选择一个更合适的版本。

动态分派

动态分派的一个最直接的例子是重写(Override)。对于重写,我们已经很熟悉了,那么Java虚拟机是如何在程序运行期间确定方法的执行版本的呢?

解释这个现象,就不得不涉及Java虚拟机的invokevirtual指令了,这个指令的解析过程有助于我们更深刻理解重写的本质。该指令的具体解析过程如下:

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

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

  3. 如果在类型C中没有找到,则按照继承关系从下到上依次对C的各个父类进行第2步的搜索和验证过程

  4. 如果始终没有找到合适的方法,则抛出抽象方法错误的异常

从这个过程可以发现,在第一步的时候就在运行期确定接收对象(执行方法的所有者称为接受者)的实际类型,所以当调用invokevirtual指令就会把运行时常量池中符号引用解析为不同的直接引用,这就是方法重写的本质。

动态分派的实例代码如下:

public class DynamicDispatch {

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();
        woman.sayHello();
        man = new Woman();
        man.sayHello();
    }

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

输出:

man say hello
woman say hello
woman say hello

虚拟机动态分派的实现

上面的叙述已经把虚拟机重写与重载的本质讲清楚了,那么Java虚拟机是如何做到这点的呢?

由于动态分派是非常频繁的操作,实际实现中不可能真正如此实现。Java虚拟机是通过“稳定优化”的手段——在方法区中建立一个虚方法表(Virtual Method Table),通过使用方法表的索引来代替元数据查找以提高性能。虚方法表中存放着各个方法的实际入口地址(由于Java虚拟机自己建立并维护的方法表,所以没有必要使用符号引用,那不是跟自己过不去嘛),如果子类没有覆盖父类的方法,那么子类的虚方法表里面的地址入口与父类是一致的;如果重写父类的方法,那么子类的方法表的地址将会替换为子类实现版本的地址。

方法表是在类加载的连接阶段(验证、准备、解析)进行初始化,准备了子类的初始化值后,虚拟机会把该类的虚方法表也进行初始化。

参考
1、周志明,深入理解Java虚拟机:JVM高级特性与最佳实践,机械工业出版社

版权声明:本文为博主原创文章,转载请注明作者和出处。 https://blog.csdn.net/u011080472/article/details/51334288

Java基础(二)

Java基础(二) 1、哪些情况下的对象会被垃圾回收机制处理掉? 讲一下常见编码方式? utf-8编码中的中文占几个字节;int型几个字节? 静态代理和动态代理的区别,什么场景...
  • black_dreamer
  • black_dreamer
  • 2018-03-08 11:52:48
  • 162

Android(2017-2018)BAT面试题整理(java篇,含答案)

Android(2017-2018)BAT面试题整理(Java篇,含答案) 版权声明:本文为博主原创文章,未经博主允许不得转载。 原文链接:http://blog.csdn.net/huangqi...
  • huangqili1314
  • huangqili1314
  • 2018-03-06 23:51:52
  • 7909

java深入源码级的面试题(有难度)

1,哪些情况下的对象会被垃圾回收机制处理掉?2,讲一下常见编码方式?3,utf-8 编码中的中文占几个字节;int型几个字节?4,静态代理和动态代理的区别,什么场景使用?5,java的异常体系6,谈谈...
  • fajuary
  • fajuary
  • 2018-03-27 20:05:44
  • 15

最全的BAT大厂面试题整理

注:本文为转载文章,原文出处: 最全的BAT大厂面试题整理前言临近年关,又到了面试求职高峰期,最近有很多网友都在求大厂面试题。正好我之前电脑里面有这方面的整理,于是就发上来分享给大家。这些题目是网友去...
  • Calvin_zhou
  • Calvin_zhou
  • 2018-03-05 18:07:21
  • 431

java中的方法调用-解析与分派

在本文开始之前,先搞清楚一个概念。对于People p = new Man();这句代码,我们把People叫做静态类型,Man叫做动态类型。静态类型在编译期可知,实际类型在运行期才可以确定下来。 解...
  • xdugucc
  • xdugucc
  • 2017-10-17 14:52:44
  • 620

深入理解java解析、分派和绑定

2,引用关系是在解析阶段确定的: 类的加载过程:加载,验证,准备,解析,初始化,使用,卸载。七个阶段顺序开始,交叉进行。 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。直接引用可以是直接指...
  • qq_32331073
  • qq_32331073
  • 2017-11-17 10:00:59
  • 115

JVM解析与分派

jvm中关于方法调用的指令有invokestatic,invokespecial,invokevirtual,invokeinterface以及invokedynamic五种。 其中invokes...
  • u013855332
  • u013855332
  • 2016-07-07 00:58:35
  • 482

算法设计与分析——任务分配问题

【问题描述】假设有n个任务需要分配给n个人执行,每个人只执行一个任务,每个任务只由一个人执行。第i个人执行第j个任务的成本是Cij(1...
  • qq_28666193
  • qq_28666193
  • 2016-11-26 16:01:22
  • 3608

深入理解java虚拟机(十一) 方法调用-解析调用与分派调用

方法调用过程是指确定被调用方法的版本(即调用哪一个方法),并不包括方法执行过程。我们知道,Class 文件的编译过程中并不包括传统编译中的连接步骤,一切方法调用在 Class 文件调用里面存储的都只是...
  • zq602316498
  • zq602316498
  • 2014-09-01 15:56:08
  • 1955

JVM详解-从入门到深入了解

之前很长一段时间都在学习JVM,但是因为时间断断续续,再加上没有经常性的使用,导致看过就忘,最近有重新复习了一遍,但是相比刚开始的时候,还是有了更好的了解。啰嗦那么多就是想告诫自己– 熟能生巧,理论...
  • hui_yan2012
  • hui_yan2012
  • 2017-04-16 11:35:17
  • 20722
收藏助手
不良信息举报
您举报文章:【深入理解JVM】:解析与分派
举报原因:
原因补充:

(最多只允许输入30个字)