Java的动态分派和静态分派的实现

Java 方法执行时的动态分派和静态分派是 Java 实现多态的本质

背景

Java 的动态分派和静态分派也是 Java 方法的执行原理。 Java 源代码的编译之后,方法之间的调用是使用符号引用来表示的。当字节码被 JVM 加载之后,符号引用才会被替换为对应方法在方法区的真实内存地址。那么在替换之前,由于 Java 的方法重写、重载,就导致符号引用对应的方法可能是一个虚方法,那么方法的真实实现在运行时就可能有多个。

所以在将符号引用替换为真实地址时,还需要做一件事情:那就是确定符号引用要替换的方法的版本。

运行时方法帧

与 C,C++ 一样,JVM 在运行时也会维护一个运行栈,用于方法的调用和返回。当调用一个方法时,会为方法在栈上分配一块内存区域作为方法的帧。方法调用帧又分为下面几个区域:

局部变量表

存储方法参数和方法体中的局部变量,其容量在编译期就已确定。容量的最小单位是 variable slot(变量槽)。
静态方法的局部变量数就是方法体中声明的变量数;实例方法的局部变量数会多一个,多出的一个就是我们平时在实例方法中访问的this。this 其实是编译器在编译时悄悄加到实例方法上的,而且是作为第一个参数。

操作数栈

JVM 的字节码指令执行机制是基于栈的,所以需要一个栈来存储字节码指令的操作数。

Android 的 VM 是基于寄存器的,所以没有操作栈区域。

Android VM 采用寄存器存储操作数有两个主要原因:1. 寄存器乃是 CPU 内部的高速内存, 读写寄存器是与 CPU 交互最快的方式。2. 智能手机多使用 ARM 架构的 CPU, ARM 架构的 CPU 有很多通用寄存器可使用。

动态链接

方法体中调用其他方法时,会把将要调用的方法在常量池中的符号引用,转化为将要其在方法区内存中的开始地址信息,并储存到动态链接中。

方法返回地址

一个方法执行完毕之后,线程需要值得回到哪里继续执行,方法返回地址就是存储这个信息的。返回地址一般就是当前方法的调用者的程序计数器的值(PC寄存器)。

  1. 正常完成出口: 方法正常返回时,如果有返回值,返回值会被压入调用方法的操作数栈中
  2. 异常完成出口: 当方法发生了异常,且在异常表中没有找到匹配的异常处理流程时,方法将不会有返回值

方法调用

方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法)

调用方法的指令

有以下字节码指令用于方法的调用:

指令 用途 说明
invokestatic 调用类的静态方法
invokespecfical 调用对象的构造函数和私有方法
invokevirtual 调用对象的 public/protected 的方法 可能通过继承复写的方法称做 virtual method: 表示要到运行时才能定位到真正的方法实现。通过符号引用确定虚方法直接引用的过程又叫做动态分派
invokeinterface 调用接口的方法 具体的实现类将在调用时确定
invokedynamic JDK1.7 为了让 JVM 支持动态类型语言引入的指令 让用户可以决定如何查找目标方法

符号引用到直接引用

由于 Java 的编译没有C C++ 编译过程中的链接阶段,所以 Class 文件中储存的只是符号引用,等到了在运行时才通过符号引用定位到方法区中方法代码在内存布局中的位置--直接引用。
符号引用到直接引用的替换又涉及两种方式。一种是解析,另一种是分派。解析发生在类加载的解析阶段,分派发生在编译或方法调用阶段。

解析

在类加载的解析阶段会把满足「编译期可知,运行期不可变」的方法的符号引用替换为指向方法区的直接引用,不会延迟到运行时再去完成。
满足编译期可知,运行期不可变的方法有:构造函数、私有方法、静态方法、final修饰的方法。不满足上述条件的方法的符号引用替换发生在方法调用期间。

分派 Dispatch

多态的实现原理

变量类型
理解分派之前,需要先看两个类型概念。
比如:Object obj = new String("");

静态类型

定义变量时,声明的类型。比如这里 obj 的静态类型就是 Object。静态类型在编译期的编译器就能知道。

实际类型

变量赋值时的实际类型。比如这里 obj 的实际类型就是 String。实际类型在编译期的编译器是不可知的。

静态分派

根据变量的「静态类型(外观类型)」匹配调用方法的过程称为静态分派。发生的场景为方法重载。
如下代码:

public class StaticDispatch {

 static abstract class Human { }
 static class Man extends Human { }
 static class Woman extends Human { }
 static class Child extends Human { }

 public void say(Human human) {
  System.out.println("human");
 }

 public void say(Man man) {
  System.out.println("man");
 }

 public void say(Woman woman) {
  System.out.println("woman");
 }

 public void say(Child child) {
  System.out.println("child");
 }
}
public static void main(String[] args) {
 Human man = new Man();
 Human woman = new Woman();
 Human child = new Child();

 StaticDispatch dispatch = new StaticDispatch();
 dispatch.say(man);
 dispatch.say(woman);
 dispatch.say(child);
}

main 方法的执行结果:

human
human
human

虽然 StaticDispatch 为每种 Human 的子类都重载了一个 say 方法,但是由于重载采用的是静态分派,是根据对象的静态类型做方法匹配的。所以结果全都匹配到了 public void say(Human human) 方法。main 方法编译之后的字节码:

public static main([Ljava/lang/String;)V
 NEW method_invoke/StaticDispatch$Man
 DUP
 INVOKESPECIAL method_invoke/StaticDispatch$Man.<init> ()V
 ASTORE 1
 NEW method_invoke/StaticDispatch$Woman
 DUP
 INVOKESPECIAL method_invoke/StaticDispatch$Woman.<init> ()V
 ASTORE 2
 NEW method_invoke/StaticDispatch$Child
 DUP
 INVOKESPECIAL method_invoke/StaticDispatch$Child.<init> ()V
 ASTORE 3
 NEW method_invoke/StaticDispatch
 DUP
 INVOKESPECIAL method_invoke/StaticDispatch.<init> ()V
 ASTORE 4
 // 下面为调用 say
 ALOAD 4
 ALOAD 1
 INVOKEVIRTUAL method_invoke/StaticDispatch.say (Lmethod_invoke/StaticDispatch$Human;)V
 ALOAD 4
 ALOAD 2
 INVOKEVIRTUAL method_invoke/StaticDispatch.say (Lmethod_invoke/StaticDispatch$Human;)V
 ALOAD 4
 ALOAD 3
 INVOKEVIRTUAL method_invoke/StaticDispatch.say (Lmethod_invoke/StaticDispatch$Human;)V
 RETURN

从字节码也能看到,编译器确实是按照静态分派选择了匹配静态类型的 StaticDispatch.say(LStaticDispatch$Human;)V 方法,而没有按照变量的实际类型去匹配重载的方法。

public class Overload {
 public static void out(char a) { System.out.println("char " + a); }
 public static void out(int a) {System.out.println("int " + a);}
 public static void out(long a) { System.out.println("long " + a); }
 public static void out(float a) { System.out.println("float " + a); }
 public static void out(double a) { System.out.println("double " + a); }
 public static void out(Integer a) { System.out.println("integer"); }
 public static void out(Character a) { System.out.println("character"); }
 public static void out(Serializable a) { System.out.println("serializable " + a); }
 public static void out(Comparable a) { System.out.println("comparable " + a); }
 public static void out(Object a) { System.out.println("object " + a); }
 public static void out(char... a) { System.out.println("char ... " + Arrays.toString(a)); }

 public static void main(String[] args) {
  out('c');
 }
}

这段代码也是一个静态分派的例子,编译器会选择参数类型做合适的函数去调用。可以注释掉所有 out 函数,留下 out(Serializable a),你会发现程序也能成功编译和运行。如果留下Serializeable 和 Comparable 编译则会失败,提示对 out 的引用不明确。

动态分派

根据变量的「实际类型」匹配调用方法的过程称为动态分派。发生的场景为方法重写。当调用一个可能被子类重写或继承的方法时,就会触发动态分派。

public class DynamicDispatch {

 static class Human {
  public void say() {
   System.out.println("human");
  }
 }

 static class Man extends Human {
  @Override
  public void say() {
   System.out.println("man");
  }
 }

 static class Woman extends Human {
  @Override
  public void say() {
   System.out.println("woman");
  }
 }
}
public static void main(String[] args) {
 Human human = new Human();
 Human man = new Man();
 Human woman = new Woman();
 human.say();
 man.say();
 woman.say();
}

main 方法的执行结果:

human
man
woman

意料之中,所谓的多态就是这样。那多态是如何实现的?

其实多态的实现过程也就是确定被重写的方法版本的过程。main 方法编译之后的字节码:

public static main([Ljava/lang/String;)V
 NEW method_invoke/DynamicDispatch$Human
 DUP
 INVOKESPECIAL method_invoke/DynamicDispatch$Human.<init> ()V
 ASTORE 1
 NEW method_invoke/DynamicDispatch$Man
 DUP
 INVOKESPECIAL method_invoke/DynamicDispatch$Man.<init> ()V
 ASTORE 2
 NEW method_invoke/DynamicDispatch$Woman
 DUP
 INVOKESPECIAL method_invoke/DynamicDispatch$Woman.<init> ()V
 ASTORE 3
 // 下面为多态调用 say
 ALOAD 1
 INVOKEVIRTUAL method_invoke/DynamicDispatch$Human.say ()V
 ALOAD 2
 INVOKEVIRTUAL method_invoke/DynamicDispatch$Human.say ()V
 ALOAD 3
 INVOKEVIRTUAL method_invoke/DynamicDispatch$Human.say ()V
 RETURN

这里通过字节码感觉都会调用Hunman#say方法的,但是运行之后并不是。

当 JVM 执行这两行字节码时:

ALOAD 1
// 由上面 ASTORE 1 可知, 局部变量表的第一个变量是 Woman 的对象
INVOKEVIRTUAL method_invoke/DynamicDispatch$Human.say ()V
// INVOKEVIRTUAL 指令就会到 Woman 类中去寻找 say 方法

调用 say 方法时,JVM 会先去当前调用的对象的类中查找是否存在和目标方法的描述符、简单名称一样的方法,如果存在则将符号引用替换为找到的方法的直接引用,否则就向父类去查找,向父类的父类去查找..., 直到最后找不到抛出NoSuchMethod异常。

Human 的 say 方法的签名:

public void say();
 descriptor: ()V

Woman 的 say 方法的签名:

public void say();
 descriptor: ()V

可见 Woman 类的 Human 类中的 say 方法的描述符和简单名称是一样的,所以 JVM 会优先匹配 Woman 类中的方法。这也是多态调用的底层逻辑。

到此这篇关于Java的动态分派和静态分派的实现的文章就介绍到这了,更多相关Java 动态分派和静态分派内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java的动态绑定与双分派_动力节点Java学院整理

    Java的动态绑定 所谓的动态绑定就是指程执行期间(而不是在编译期间)判断所引用对象的实际类型,根据其实际的类型调用其相应的方法.java继承体系中的覆盖就是动态绑定的,看一下如下的代码: class Father { public void method(){ System.out.println("This is Father's method"); } } class Son1 extends Father{ public void method(){ System.out.pr

  • Java的动态分派和静态分派的实现

    Java 方法执行时的动态分派和静态分派是 Java 实现多态的本质 背景 Java 的动态分派和静态分派也是 Java 方法的执行原理. Java 源代码的编译之后,方法之间的调用是使用符号引用来表示的.当字节码被 JVM 加载之后,符号引用才会被替换为对应方法在方法区的真实内存地址.那么在替换之前,由于 Java 的方法重写.重载,就导致符号引用对应的方法可能是一个虚方法,那么方法的真实实现在运行时就可能有多个. 所以在将符号引用替换为真实地址时,还需要做一件事情:那就是确定符号引用要替换的

  • java JVM方法分派模型静态分派动态分派全面讲解

    前言 了解 行为方法分派 有利于在行为分派时时进行一些功能操作 本文全面讲解行为分派的类型:静态 & 动态行为分派,希望你们会喜欢. 目录 1. 知识储备 1.1 分派 定义:确定执行哪个方法 的过程 a. 疑问 有些读者会问,方法的执行不是取决于代码设置中的执行对象吗?为什么还要选择呢? b. 回答 若 一个对象对应于多个方法 时,就需要进行选择了 读者应该都想到了 Java中的特性:多态,即重写 & 重载.下面我会详细讲解. 分类:静态分派 & 动态分派.下面我将详细讲解. 1

  • Java的动态代理和静态代理详解

    目录 0.代理模式 1.静态代理 2.加深理解 3.动态代理 动态代理的例子 总结 0.代理模式 为什么要学习代理模式?这是SpringAOP的底层[SpringAOP和SpringMVC] 代理模式的分类: 静态代理 动态代理 1.静态代理 静态代理中,我们对目标对象的每个方法的增强都是手动完成的(后面会具体演示代码_),非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(_需要对每个目标类都单独写一个代理类). 实际应用场景非常非常少,日常开发几乎看不到使用静态代理的

  • 使用java将动态网页生成静态网页示例

    复制代码 代码如下: package com.tools;import java.io.*;import java.net.URL; /** * Title:动态页面静态化 */public class GoToHtml {/** *  * @param page *            存放静态页面的本地文件路径(c,d,e,f,g) * @param url_addr *            所要生成的静态页的URL地址(http://) * @return */public boole

  • JVM 方法调用之静态分派(详解)

    分派(Dispatch)可能是静态也可能是动态的,根据分派依据的宗量数可分为单分派和多分派.这两种分派方式的两两组合就构成了静态单分派,静态多分派,动态单分派,动态多分派这4种组合.本章讲静态分派. 1.静态分派 所有依赖静态类型来定位方法执行版本的分派动作称为静态分派.静态分派的典型应用是方法重载.静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的. 那么什么是静态类型(static type)呢? Super object = new Sub(); 像上面的语句,Sup

  • Java方法调用解析静态分派动态分派执行过程

    方法调用 在程序运行时,进行方法调用是最普遍,最频繁的操作 方法调用不等于方法执行: 方法调用阶段唯一的任务就是确定被调用的方法版本,即调用哪一个方法 不涉及方法内部的具体运行过程 Class文件的编译过程不包括传统编译中的连接步骤 Class文件中的一切方法调用在Class文件里面存储的都是符号引用,而不是方法在在实际运行时内存布局中的入口地址,即之前的直接引用: 这样使得Java具有更强大的动态扩展能力 同时也使得Java方法调用过程变得相对复杂 需要在类加载期间,甚至会到运行期间才能确定目

  • 深入理解Java动态代理与静态代理

    目录 前言 一.静态代理 静态代理的使用 与装饰者模式的区别 二.动态代理 JDK 动态代理 CGlib 动态代理实现 前言 学习 Spring 的过程中,不可避免要掌握代理模式.这篇文章总结一下代理模式.顾名思义,代理,就是你委托别人帮你办事,所以代理模式也有人称作委托模式的.比如领导要做什么事,可以委托他的秘书去帮忙做,这时就可以把秘书看做领导的代理.下面将以这个例子来讲解.代理模式又分为静态代理和动态代理. 一.静态代理 静态代理的使用 静态代理,代理类和被代理的类实现了同样的接口,代理类

  • Java JDK动态代理(AOP)的实现原理与使用详析

    本文主要给大家介绍了关于Java JDK动态代理(AOP)实现原理与使用的相关内容,分享出来供大家参考学习,下面来一起看看详细的介绍: 一.什么是代理? 代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问.代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理. 代理模式UML图: 简单结构示意图: 为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别.通过代理类这中间一层,能有效控制对委托类对

  • 详解Java JDK动态代理

    今天来看看Java的另一种代理方式--JDK动态代理 我们之前所介绍的代理方式叫静态代理,也就是静态的生成代理对象,而动态代理则是在运行时创建代理对象.动态代理有更强大的拦截请求功能,因为可以获得类的运行时信息,可以根据运行时信息来获得更为强大的执(骚)行(操)力(作). 我们还是以上一个例子为例,这里的IStars接口和Stars类都不需要修改,只需要修改代理类. 创建JDK动态代理需要先实现InvocationHandler接口,并重写其中的invoke方法,具体步骤如下: 1. 创建一个类

  • Java 8 动态类型语言Lambda表达式实现原理解析

    Java 8支持动态语言,看到了很酷的Lambda表达式,对一直以静态类型语言自居的Java,让人看到了Java虚拟机可以支持动态语言的目标. import java.util.function.Consumer; public class Lambda { public static void main(String[] args) { Consumer<String> c = s -> System.out.println(s); c.accept("hello lambd

随机推荐