深入理解JVM之七:静态分派与动态分派

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介:

前言

这里所谓的分派指的是在Java中对方法的调用。Java中有三大特性:封装、继承和多态。分派是多态性的体现,Java虚拟机底层提供了我们开发中“重写”和“重载”的底层实现。其中重载属于静态分派,而重写则是动态分派的过程。除了使用分派的方式对方法进行调用之外,还可以使用解析调用,解析调用是在编译期间就已经确定了,在类装载的解析阶段就会把符号引用转化为直接引用,不会延迟到运行期间再去完成。而分派调用则既可以是静态的也可以是动态(就是这里的静态分派和动态分派)的。

1.静态分派

静态分派只会涉及重载,而重载是在编译期间确定的,那么静态分派自然是一个静态的过程(因为还没有涉及到Java虚拟机)。静态分派的最直接的解释是在重载的时候是通过参数的静态类型而不是实际类型作为判断依据的。比如创建一个类O,在O中创建了静态类内部类A,O中又有两个静态类内部类B、C继承了这个静态内部类A,那么实际上当编写如下的代码:

public class O{
    static class A{}
    static class B extends A{}
    static class C extends A{}
    public void a(A a){
        System.out.println("A method");
    }
    public void a(B b){
        System.out.println("B method");
    }
    public void a(C c){
        System.out.println("C method");
    }
    public static void main(String[] args){
        O o = new O();
        A b = new B();
        A c = new C();
        o.a(b);
        o.a(c);
    }
}

运行的结果是打印出连个“A method”。原因在于静态类型的变化仅仅在使用时发生,变量本省的类型不会发生变化。比如我们这里中A b = new B();虽然在创建的时候是B的对象,但是当调用o.a(b)的时候才发现是A的对象,所以会输出“A method”。也就是说在发生重载的时候,Java虚拟机是通过参数的静态类型而不是实际参数类型作为判断依据的。因此,在编译阶段,Javac编译器选择了a(A a)这个重载方法。

虽然编译器能够在编译阶段确定方法的版本,但是很多情况下重载的版本不是唯一的,在这种模糊的情况下,编译器会选择一个更合适的版本。

2.动态分派

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

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

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

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

3.虚拟机动态分派的实现

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

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

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

目录
相关文章
|
6月前
|
监控 Java
jvm性能调优实战 - 24模拟因动态年龄判断对象进入老年代的场景
jvm性能调优实战 - 24模拟因动态年龄判断对象进入老年代的场景
97 0
|
4月前
|
存储 缓存 自然语言处理
(三)JVM成神路之全面详解执行引擎子系统、JIT即时编译原理与分派实现
执行引擎子系统是JVM的重要组成部分之一,在JVM系列的开篇曾提到:JVM是一个架构在平台上的平台,虚拟机是一个相似于“物理机”的概念,与物理机一样,都具备代码执行的能力。
|
6月前
|
算法 Oracle Java
【JVM】了解JVM中动态判断对象年龄的原理
【JVM】了解JVM中动态判断对象年龄的原理
136 0
|
6月前
|
存储 Java 编译器
深入理解JVM - 栈帧和分派
深入理解JVM - 栈帧和分派
233 0
|
存储 Java 编译器
深入理解JVM - 栈帧和分派
关于jvm的大部分内容系列文章都已经讨论过了,这个系列也将近尾声,本文将会讲述关于jvm是如何实现重载和重写的,以及栈桢的内部存放的内容,这部分内容是非常重要的,也是面试有可能问到的一些高频面试点。
53 0
|
26天前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
36 4
|
26天前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
51 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
29天前
|
存储 缓存 算法
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
|
16天前
|
存储 算法 Java
聊聊jvm的内存结构, 以及各种结构的作用
【10月更文挑战第27天】JVM(Java虚拟机)的内存结构主要包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和运行时常量池。各部分协同工作,为Java程序提供高效稳定的内存管理和运行环境,确保程序的正常执行、数据存储和资源利用。
43 10