Java方法调用解析静态分派动态分派执行过程
作者:攻城狮Chova 发布时间:2023-05-03 04:32:40
方法调用
在程序运行时,进行方法调用是最普遍,最频繁的操作
方法调用不等于方法执行:
方法调用阶段唯一的任务就是确定被调用的方法版本,即调用哪一个方法
不涉及方法内部的具体运行过程
Class文件的编译过程不包括传统编译中的连接步骤
Class文件中的一切方法调用在Class文件里面存储的都是符号引用,而不是方法在在实际运行时内存布局中的入口地址,即之前的直接引用:
这样使得Java具有更强大的动态扩展能力
同时也使得Java方法调用过程变得相对复杂
需要在类加载期间,甚至会到运行期间才能确定目标方法的直接引用
方法解析
所有方法调用中的目标方法在Class文件里都是一个常量池的引用
在类的加载解析阶段,会将其中的一部分符号引用转化为直接引用:
方法在程序真正执行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的
也就是说,调用目标在程序代码中完成,编译器进行编译时就必须确定下来,这也叫做方法解析
Java方法分类
在Java中符合 "编译期可知,运行期不可变" 的方法有两大类:
静态方法: 与类型直接关联
私有方法: 在外部不可被访问
这两种方法各自的特点决定这两种方法都不可能通过继承或者别的方式重写版本,因此适合在类加载阶段进行解析
非虚方法: 在类加载阶段会把符号引用解析为该方法的直接引用
静态方法
私有方法
实例构造器
父类方法
虚方法: 在类加载阶段不会将符号引用解析为该方法的直接引用
除去以上的非虚方法,其它的方法均为虚方法
静态分派
public class StaticDispatch {
static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
public static void sayHello(Human guy) {
System.out.println("Hello, Guy!");
}
public static void sayHello(Man guy) {
System.out.println("Hello, Gentleman!");
}
public static void sayHello(woman guy) {
System.out.println("Hello, Lady!");
}
public static void main(String[] args) {
Human man = new Man();
Human women = new Woman();
sayHello(man);
sayHello(woman);
}
}
Human man = new Human();
Human
为变量的静态类型
Man
为变量的实际类型
静态类型和实际类型在程序中都会放生变化:
静态类型:
静态类型的变化仅仅在使用时发生
变量本身的静态类型不会被改变
最终的静态类型在编译器中可知
实际类型:
实际类型变化的结果在运行期才确定下来
编译器在编译期间并不知道一个对象的实际类型是什么
Human human = new Man();
sayHello(man);
sayHello((Man)man);// 类型转换,静态类型变化,转型后的静态类型一定是Man
man = new woman();// 实际类型变化,实际类型是不确定的
sayHello(man);
sayHello((Woman)man);// 类型转换,静态类型变化
编译器在重载时是通过参数的静态类型而不是实际类型作为判断依据,静态类型在编译期间可以知道:
编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本
静态分派:
所有依赖静态类型来定位方法的执行版本的分派动作
典型应用 :方法重载
静态分派发生在编译阶段,因此确定静态分派的的动作不是由虚拟机执行的,而是由编译器完成的
由于字面量没有显示静态类型,只能通过语言上的规则去理解和推断
public class LiteralTest {
public static void sayHello(char arg) {
System.out.println("Hello, char!");
}
public static void sayHello(int arg) {
System.out.println("Hello, int!");
}
public static void sayHello(long arg) {
System.out.println("Hello, long!");
}
public static void sayHello(Character arg) {
System.out.println("Hello, Character!");
}
public static void main(String[] arg) {
sayHello('a');
}
}
编译器将重载方法从上向下依次注释,得到不同的输出
如果编译器无法确定要自定转型为哪种类型,会提示类型模糊,拒绝编译
public class LiteralTest {
public static void sayHello(String arg) {// 新增重载方法
System.out.println("Hello, String!");
}
public static void sayHello(char arg) {
System.out.println("Hello, char!");
}
public static void sayHello(int arg) {
System.out.println("Hello, int!");
}
public static void sayHello(long arg) {
System.out.println("Hello, long!");
}
public static void sayHello(Character arg) {
System.out.println("Hello, Character!");
}
public static void main(String[] args) {
Random r = new Random();
String s = "abc";
int i = 0;
sayHello(r.nextInt() % 2 != 0 ? s : 1 );// 编译错误
sayHello(r.nextInt() % 2 != 0 ? 'a' : false);//编译错误
}
}
动态分派
public class DynamicDispatch {
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!");
}
}
public static void main(String[] args) {
Human man = new Man();
Human women = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
}
这里不是根据静态类型决定的
静态类型的Human两个变量man和woman在调用sayHello() 方法时执行了不同的行为
变量man在两次调用中执行了不同的方法
导致这个现象的额原因 :这两个变量的实际类型不同
Java虚拟机是如何根据实际类型分派方法的执行版本的: 从invokevirtual指令的多态查找过程开始 ,invokevirtual指令运行时解析过程大致分为以下几个步骤:
找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C
如果在类型C中找到与常量中的描述符和简单名称相符合的方法,然后进行访问权限验证,如果验证通过则返回这个方法的直接引用,查找过程结束;如果验证不通过,则抛出java.lang.illegalAccessError异常
如果未找到,就按照继承关系从下往上依次对类型C的各个父类进行第二步的搜索和验证过程
如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常
Java语言方法重写的本质:
invokevirtual指令执行的第一步就是在运行时期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上
这种在运行时期根据实际类型确定方法执行版本的分派过程就叫做动态分派
虚拟机动态分派的实现
虚拟机概念解析的模式就是静态分派和动态分派,可以理解虚拟机在分派中 "会做什么" 这个问题
虚拟机 "具体是如何做到的" 在各种虚拟机实现上会有差别:
由于动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法
因此在虚拟机的实际实现中,为了基于性能的考虑,大部分实现都不会真正的进行如此频繁的搜索
最常用的"稳定优化"的方式是为类在方法区中建立一个虚方法表(Virtual Method Table,即vtable), 使用虚方法表索引代替元数据查找以提高性能
虚方法表中存放着各个方法的实际入口地址:
如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实际入口
如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实际方法的入口地址
具有相同签名的方法,在父类,子类的虚方法表中具有一样的索引序号:
这样当类型变换时,仅仅需要变更查找的方法表,就可以从不同的虚方法表中按索引转换出所需要的入口地址
方法表一般在类加载阶段的连接阶段进行初始化:
准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕
来源:https://cloud.tencent.com/developer/article/1938329?from=article.detail.1905407


猜你喜欢
- 1、SSL介绍和说明SSL的配置也是我们在实际应用中经常遇到的场景SSL(Secure Sockets Layer,安全套接层)是为网络通信
- 一、简介线程安全概念:线程安全是指在当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出
- app_main上一篇文章:# Android 10 启动分析之servicemanager篇 (二)在init篇中有提到,init进程会在
- 本文实例讲述了 Android 7.0开发获取存储设备信息的方法。分享给大家供大家参考,具体如下:Android 7.0开发相较之前有不少改
- 本文实例为大家分享了java将某个数据库的表全部导出到excel中的方法,供大家参考,具体内容如下第一步:如何用POI操作Excel@Tes
- ViewPager2 介绍ViewPager2 是基于 RecyclerView 重新编写的 ViewPager,比原有的 ViewPage
- 目录1、备份原数据库File文件2、数据库升级XML编写 updateXml.xml3、创建XML解析器3.1 对应工具类 DomUtils
- 常见的EditText长按菜单如下oppo小米需求是隐藏掉其中的分享/搜索功能,禁止将内容分享到其他应用。最终解决方案这里先说下最终解决方案
- 下面还有投票,帮忙投个票👍前言最近在看某个开源项目代码并准备参与其中,代码过了一遍后发现多个自定义的配置文件用来装载业务配置代替数据库查询,
- 前言:上篇总结了下WebApi的接口测试工具的使用,这篇接着来看看WebAPI的另一个常见问题:跨域问题。本篇主要从实例的角度分享下CORS
- 本文实例介绍了sdcard存储图片下载简单操作,分享给大家供大家参考,具体内容如下步骤 -- 在配置清单添加完联网权限后1、res/layo
- 本文实例讲述了Hibernate实现批量添加数据的方法。分享给大家供大家参考,具体如下:1.Hibernate_016_Ba
- C# 8.0中的模式匹配相对C# 7.0来说有了进一步的增强,对于如下类:class Point{ public
- 1.项目介绍本项目旨在打造一个基于RBAC架构模式的通用的、并不复杂但易用的权限管理系统。通过本项目可以较好的理解权限系统的常见业务同时学习
- 本文实例讲述了C#实现导出List数据到xml文件的方法。分享给大家供大家参考,具体如下:C#导出List数据到xml文件,这里主要用到的是
- 单点登录概念单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系
- 数组排序在很多的面试题上都会出现数组排序的操作形式。但是这个时候你千万别写上:java.util.Arrays.sort(数组)。而这种排序
- 在框架开发过程中,通用代码生成是一项必不可少的功能,c#在这后端模板引擎这方面第三方组件较少,我这里选择的是NVelocity,现在升级到了
- 阿里终面在线编程题,写出来与大家分享一下 有一个单向链表
- 本文实例讲述了C#基于简单工厂模式实现的计算器功能。分享给大家供大家参考,具体如下:子类拥有父类除私有之外的所有属性字段和方法using S