【JVM补充】静态多分派,动态单分派

2023-12-10 11:38

本文主要是介绍【JVM补充】静态多分派,动态单分派,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

背景

之前在看过第二版的《深入理解JVM虚拟机》之后,对 java是一种静态多分派和动态单分派的语言 相关的说明和介绍,我理解的就不是很清晰,所以在前面的 JVM详解 一章中没有详细说明这一块内容,而关注到这个问题,最早还是在虚拟机栈中的 动态链接 内存分配的地方存在疑问,最近在看第三版的书,又看到了这个地方,所以在这里记录一下个人理解,以便之后回顾,如果有错误的地方,欢迎提出。

示例

package com.demo;public class OverLoadTest {static class Mobile{}static class Phone extends Mobile{}static class Pad extends Mobile{}static class TX{public void test(Mobile mobile){System.out.println("tx-mobile");}public void test(Phone phone){System.out.println("tx-phone");}public void test(Pad pad){System.out.println("tx-pad");}}static class QQ extends TX{@Overridepublic void test(Mobile mobile){System.out.println("qq-mobile");}@Overridepublic void test(Phone phone){System.out.println("qq-phone");}@Overridepublic void test(Pad pad){System.out.println("qq-pad");}}static class WeChat extends TX{@Overridepublic void test(Mobile mobile){System.out.println("wc-mobile");}@Overridepublic void test(Phone phone){System.out.println("wc-phone");}@Overridepublic void test(Pad pad){System.out.println("wc-pad");}}public static void main(String[] args) {TX qq = new QQ();TX wc = new WeChat();Mobile phone = new Phone();Mobile pad = new Pad();qq.test(phone);wc.test(pad);}}

打印输出结果:

qq-mobile
wc-mobile

看到这个结果,相信很多人都能猜到,但是为什么是这个结果,让我们来剖析一下,剖析完之后,大家就能明白其细节的原理了。

编译期间

首先,我们来看一下对当前类编译之后的结果:

命令:
javac OverLoadTest.java
javap -verbose OverLoadTest.class

Classfile /E:/jvm-demo/src/main/java/com/demo/OverLoadTest.classLast modified 2021-5-7; size 724 bytesMD5 checksum ac452de465b1d19e57ff6450d8660163Compiled from "OverLoadTest.java"
public class com.demo.OverLoadTestminor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 = Methodref          #12.#30        // java/lang/Object."<init>":()V#2 = Class              #31            // com/demo/OverLoadTest$QQ#3 = Methodref          #2.#30         // com/demo/OverLoadTest$QQ."<init>":()V#4 = Class              #32            // com/demo/OverLoadTest$WeChat#5 = Methodref          #4.#30         // com/demo/OverLoadTest$WeChat."<init>":()V#6 = Class              #33            // com/demo/OverLoadTest$Phone#7 = Methodref          #6.#30         // com/demo/OverLoadTest$Phone."<init>":()V#8 = Class              #34            // com/demo/OverLoadTest$Pad#9 = Methodref          #8.#30         // com/demo/OverLoadTest$Pad."<init>":()V#10 = Methodref          #16.#35        // com/demo/OverLoadTest$TX.test:(Lcom/demo/OverLoadTest$Mobile;)V#11 = Class              #36            // com/demo/OverLoadTest#12 = Class              #37            // java/lang/Object#13 = Utf8               WeChat#14 = Utf8               InnerClasses#15 = Utf8               QQ#16 = Class              #38            // com/demo/OverLoadTest$TX#17 = Utf8               TX#18 = Utf8               Pad#19 = Utf8               Phone#20 = Class              #39            // com/demo/OverLoadTest$Mobile#21 = Utf8               Mobile#22 = Utf8               <init>#23 = Utf8               ()V#24 = Utf8               Code#25 = Utf8               LineNumberTable#26 = Utf8               main#27 = Utf8               ([Ljava/lang/String;)V#28 = Utf8               SourceFile#29 = Utf8               OverLoadTest.java#30 = NameAndType        #22:#23        // "<init>":()V#31 = Utf8               com/demo/OverLoadTest$QQ#32 = Utf8               com/demo/OverLoadTest$WeChat#33 = Utf8               com/demo/OverLoadTest$Phone#34 = Utf8               com/demo/OverLoadTest$Pad#35 = NameAndType        #40:#41        // test:(Lcom/demo/OverLoadTest$Mobile;)V#36 = Utf8               com/demo/OverLoadTest#37 = Utf8               java/lang/Object#38 = Utf8               com/demo/OverLoadTest$TX#39 = Utf8               com/demo/OverLoadTest$Mobile#40 = Utf8               test#41 = Utf8               (Lcom/demo/OverLoadTest$Mobile;)V
{public com.demo.OverLoadTest();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 3: 0public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=5, args_size=10: new           #2                  // class com/demo/OverLoadTest$QQ3: dup4: invokespecial #3                  // Method com/demo/OverLoadTest$QQ."<init>":()V7: astore_18: new           #4                  // class com/demo/OverLoadTest$WeChat11: dup12: invokespecial #5                  // Method com/demo/OverLoadTest$WeChat."<init>":()V15: astore_216: new           #6                  // class com/demo/OverLoadTest$Phone19: dup20: invokespecial #7                  // Method com/demo/OverLoadTest$Phone."<init>":()V23: astore_324: new           #8                  // class com/demo/OverLoadTest$Pad27: dup28: invokespecial #9                  // Method com/demo/OverLoadTest$Pad."<init>":()V31: astore        433: aload_134: aload_335: invokevirtual #10                 // Method com/demo/OverLoadTest$TX.test:(Lcom/demo/OverLoadTest$Mobile;)V38: aload_239: aload         441: invokevirtual #10                 // Method com/demo/OverLoadTest$TX.test:(Lcom/demo/OverLoadTest$Mobile;)V44: returnLineNumberTable:line 59: 0line 60: 8line 62: 16line 63: 24line 65: 33line 66: 38line 67: 44
}
SourceFile: "OverLoadTest.java"
InnerClasses:static #13= #4 of #11; //WeChat=class com/demo/OverLoadTest$WeChat of class com/demo/OverLoadTeststatic #15= #2 of #11; //QQ=class com/demo/OverLoadTest$QQ of class com/demo/OverLoadTeststatic #17= #16 of #11; //TX=class com/demo/OverLoadTest$TX of class com/demo/OverLoadTeststatic #18= #8 of #11; //Pad=class com/demo/OverLoadTest$Pad of class com/demo/OverLoadTeststatic #19= #6 of #11; //Phone=class com/demo/OverLoadTest$Phone of class com/demo/OverLoadTeststatic #21= #20 of #11; //Mobile=class com/demo/OverLoadTest$Mobile of class com/demo/OverLoadTest

常量池和版本信息等我们就不关注了,我们主要看一下下面红框里的字节码,下面这两行编译后的指令对应的是java代码中的:


从上面我们能看出来,不管是方法的调用者,还是方法的参数,我们都选择的是子类的实例,但是编译之后的字节码能看出来,方法的入参和调用者都是父类,这是什么原因呢:
让我们先了解几个概念:

  1. 宗量:方法的入参和方法的调用者都是宗量
  2. 多分派:多于一个宗量的选择就是多分派
  3. 单分派:只针对一个宗量的选择就是单分派

由于在编译期间,编译器无法确定方法的最终调用者是哪一个子类,也无法确定方法的入参使用的是哪一个子类,所以都是选择的作为 静态变量 的父类进行方法的分派,也就是说,静态变量是 编译器可知,运行期不可变 的属性,但是可能有些同学还会有疑问,让我们改一下方式看看:

TX qq = new Random().nextInt() % 2 == 0 ? new QQ() : new WeChat();

从这刚代码,相信大家更能明白为什么编译期间选择静态变量的父类,而不是子类,因为只有在运行期间,才能确定创建的是哪一个子类实例。
所以从上面的例子能看出来,编译期间是针对方法的调用者和入参的 静态多分派 实现。
而最典型的例子就是方法的 重载

运行期间

从上面编译之后的字节码看到的是,方法的调用者和方法的入参都是父类,那为什么打印的结果不是tx.mobile呢,为什么方法的调用者都变成了子类呢,这是因为在代码运行期间,虚拟机能够确定方法的 最终调用者 是哪一个子类实例,所以 动态的分派 到对应子类 重写的方法 中去,所以在运行期间,针对方法的调用者这一宗量,JVM会采用 动态单分派 的方式进行方法的分派。
而典型的例子就是方法的 重写
综上,我们就能理解,为什么说 java是一门静态多分派,动态单分派的语言 了。

扩展

我们知道,在JVM 类加载期间解析阶段 执行的是 符号引用转化为直接引用 的过程,在解析字节码文件的时候,会将字面量转换为真正引用的内存地址,而从上面的介绍我们又了解到,像方法的重写类似的逻辑,在这里是无法直接转换的,这里转换的只能是 非虚方法或者是非虚引用 也就是我们常说的静态方法,私有方法或者父类方法等,但是在该案例中,这两个方法的指令是: invokevirtual ,他表示的是调用的虚方法,所以在这里是无法直接转换的。
所以这就涉及到了虚拟机栈中的 动态链接 ,在运行期间,执行该栈帧中的代码,由于这是一个 虚方法的调用 ,所以在动态链接这里才真正的将方法的调用者分派到子类实例中去,实现子类方法重写的执行。

思考

之前在学习到动态链接的地方,由于不是很理解,回过头思考类加载时期的解析逻辑,再联系到编译时期的静态多分派。
由静态多分派,动态单分派,再一步步正推回动态链接。
果然,之前学习时跳过的坑,总是要回头填上的。

这篇关于【JVM补充】静态多分派,动态单分派的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/477008

相关文章

springboot集成easypoi导出word换行处理过程

《springboot集成easypoi导出word换行处理过程》SpringBoot集成Easypoi导出Word时,换行符n失效显示为空格,解决方法包括生成段落或替换模板中n为回车,同时需确... 目录项目场景问题描述解决方案第一种:生成段落的方式第二种:替换模板的情况,换行符替换成回车总结项目场景s

SpringBoot集成redisson实现延时队列教程

《SpringBoot集成redisson实现延时队列教程》文章介绍了使用Redisson实现延迟队列的完整步骤,包括依赖导入、Redis配置、工具类封装、业务枚举定义、执行器实现、Bean创建、消费... 目录1、先给项目导入Redisson依赖2、配置redis3、创建 RedissonConfig 配

SpringBoot中@Value注入静态变量方式

《SpringBoot中@Value注入静态变量方式》SpringBoot中静态变量无法直接用@Value注入,需通过setter方法,@Value(${})从属性文件获取值,@Value(#{})用... 目录项目场景解决方案注解说明1、@Value("${}")使用示例2、@Value("#{}"php

SpringBoot分段处理List集合多线程批量插入数据方式

《SpringBoot分段处理List集合多线程批量插入数据方式》文章介绍如何处理大数据量List批量插入数据库的优化方案:通过拆分List并分配独立线程处理,结合Spring线程池与异步方法提升效率... 目录项目场景解决方案1.实体类2.Mapper3.spring容器注入线程池bejsan对象4.创建

线上Java OOM问题定位与解决方案超详细解析

《线上JavaOOM问题定位与解决方案超详细解析》OOM是JVM抛出的错误,表示内存分配失败,:本文主要介绍线上JavaOOM问题定位与解决方案的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录一、OOM问题核心认知1.1 OOM定义与技术定位1.2 OOM常见类型及技术特征二、OOM问题定位工具

基于 Cursor 开发 Spring Boot 项目详细攻略

《基于Cursor开发SpringBoot项目详细攻略》Cursor是集成GPT4、Claude3.5等LLM的VSCode类AI编程工具,支持SpringBoot项目开发全流程,涵盖环境配... 目录cursor是什么?基于 Cursor 开发 Spring Boot 项目完整指南1. 环境准备2. 创建

Spring Security简介、使用与最佳实践

《SpringSecurity简介、使用与最佳实践》SpringSecurity是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架,本文给大家介绍SpringSec... 目录一、如何理解 Spring Security?—— 核心思想二、如何在 Java 项目中使用?——

SpringBoot+RustFS 实现文件切片极速上传的实例代码

《SpringBoot+RustFS实现文件切片极速上传的实例代码》本文介绍利用SpringBoot和RustFS构建高性能文件切片上传系统,实现大文件秒传、断点续传和分片上传等功能,具有一定的参考... 目录一、为什么选择 RustFS + SpringBoot?二、环境准备与部署2.1 安装 RustF

springboot中使用okhttp3的小结

《springboot中使用okhttp3的小结》OkHttp3是一个JavaHTTP客户端,可以处理各种请求类型,比如GET、POST、PUT等,并且支持高效的HTTP连接池、请求和响应缓存、以及异... 在 Spring Boot 项目中使用 OkHttp3 进行 HTTP 请求是一个高效且流行的方式。

java.sql.SQLTransientConnectionException连接超时异常原因及解决方案

《java.sql.SQLTransientConnectionException连接超时异常原因及解决方案》:本文主要介绍java.sql.SQLTransientConnectionExcep... 目录一、引言二、异常信息分析三、可能的原因3.1 连接池配置不合理3.2 数据库负载过高3.3 连接泄漏