深入理解java虚拟机:虚拟机字节码执行引擎(2)

news2025/1/16 2:04:17

文章目录

  • 3. 方法调用
    • 3.1 解析
    • 3.2 分派

接着深入理解java虚拟机:虚拟机字节码执行引擎(1),我们继续往下看:

3. 方法调用

方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行流程。在程序运行时,进行方法调用是最普遍、最频繁的操作,但前面已经讲过,Class文件的编译过程中 不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是 符号引用,而不是方法在实际运行时内存布局中的 入口地址(相当于之前所说的直接引用)。这个特性给Java带来了更强大的动态扩展能力,但也使得Java方法的调用过程变得相对复杂起来,需要在类加载期间甚至到运行期间才能确定目标方法的直接引用。

3.1 解析

所有方法调用中的目标方法在Class文件里面都是一个 常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的 调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为 解析(Resolution)

在Java语言中,符合“编译期可知,运行期不可变”这个要求的方法主要有 静态方法私有方法 两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法都不能通过继承或者别的方式重写出其他版本,因此它们都适合在类加载阶段进行解析。

与之对应,在Java虚拟机里面提供了4条方法调用字节码指令,分别是

  1. invokestatic:调用静态方法
  2. invokespecial:调用实例构造器<init>方法、私有方法和父类方法
  3. invokevirtual:调用所有虚方法
  4. invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象

只要能被invokestaticinvokespecial指令调用的方法,都可以在解析阶段 确定唯一的调用版本,符合这个条件的有 静态方法私有方法实例构造器父类方法 四类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可以称为 非虚方法,与之相反,其他方法就称为虚方法(除去final方法,后文会提到)。下面验收一个最常见的解析调用例子,此样例中,静态方法sayHello()只可能属于类型StaticResolution,没有任何手段可以覆盖或隐藏这个方法。

/*
* 方法静态解析演示
*/
public class Test {

    public static void sayHello(){
        System.out.println("hello world");
    }

    public static void main(String[] args) {
        Test.sayHello();
    }
}
/*
javap -verbose Test

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: invokestatic  #5                  // Method sayHello:()V
         3: return
      LineNumberTable:
        line 7: 0
        line 8: 3
*/

使用javap命令查看这段程序的字节码,会发现的确是通过invokestatic命令来调用 sayHello() 方法的

Java中的非虚方法除了使用invokestaticinvokespecial调用的方法之外还有一种,就是被 final修饰的方法。虽然final方法是使用invokevirtual指令来调用的,但是 由于它无法被覆盖,没有其他版本,所以也无须对方法接收者进行多态选择,又或者说多态选择的结果肯定是唯一的。在Java语言规范中明确说明了**final方法是一种非虚方法**。

解析调用一定是个静态的过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期再去完成。而分派(Dispatch)调用则可能是静态的也可能是动态的,根据分派依据的宗量数可分为单分派和多分派。这两类分派方式两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派四种分派情况,下面我们看看虚拟机中的方法分派是如何进行的。

3.2 分派

众所周知,Java是一门面向对象的程序设计语言,因为Java具备面向对象的三个基本特征:继承、封装和多态。本节讲解的分派调用过程将会揭示多态性特征的一些最基本的体现(如“重载”和“重写”),在Java中是如何实现的,这里的实现当然不是语法上该如何写,我们关心的依然是虚拟机如何确定正确的目标方法。

1. 静态分派

我们从一个例子出发:

public class StaticDispatch {

    static abstract class Human{}
    static class Man extends Human{}
    static class Women extends Human{}

    public void sayHello(Human guy){
        System.out.println("hello guy");
    }

    public void sayHello(Man guy){
        System.out.println("hello, gentleman");
    }

    public void sayHello(Women guy){
        System.out.println("hello, lady");
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human women = new Women();
        StaticDispatch sd = new StaticDispatch();
        sd.sayHello(man);
        sd.sayHello(women);
    }
}
/**
hello guy
hello guy
*/

首先解释一下什么叫 重载,或者说方法的重载,指的是一个类下面的 同名不同参 的一系列函数。上面代码为什么会选择执行参数类型为 Human 的重载呢?我们先按如下代码定义两个重要的概念:

Human man = new Man();

我们把上面代码中的“Human”称为变量的 静态类型(Static Type) 或者 外观类型(Apparent Type),后面的“Man”则称为变量的 实际类型(Actual Type),静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。如下面的代码:

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

解释了这两个概念,再回到代码的样例代码中。main()里面的两次sayHello()方法调用,在方法接收者已经确定是对象“sd”的前提下,使用哪个重载版本,就完全取决于传入参数的数量和数据类型。代码中刻意地定义了两个静态类型相同、实际类型不同的变量,但虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且 静态类型是编译期可知的,所以在编译阶段,Javac编译器就根据参数的静态类型决定使用哪个重载版本,所以选择了sayHello(Human)作为调用目标,并把这个方法的符号引用写到main()方法里的两条invokevirtual指令的参数中。

所有依赖静态类型来定位方法执行版本的分派动作,都称为静态分派。静态分派的最典型应用就是 方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。另外,编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是“唯一的”,往往只能确定一个“更加合适的”版本。

public class Test {

    public static void sayHello(Object arg) {
        System.out.println("hello object");
    }
    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 sayHello(char arg) {
        System.out.println("hello char");
    }
    public static void sayHello(char... arg) {
        System.out.println("hello char...");
    }
    public static void sayHello(Serializable arg) {
        System.out.println("hello Serializable");
    }
    

    public static void main(String[] args) {
        sayHello('a');
    }
}
/*
hello char
*/

如果分别注释掉最近一次的输出方法,那么轮番输出是以下列形式呈现

/*
hello int
hello long
hello Character
hello Serializable
hello Object
hello char...
*/

按照上面的轮番注释,发现可见变长参数的重载优先级是最低的,这时候‘a'被当做一个数组元素。当然还可以选择int类型、Character类型、Object类型等的变长参数重载来把上面的过程重新演示一遍。但是要注意的是,有一些在单个参数中能成立的自动转型,如char转型为int,在变长参数中是不成立的。

另外还有一点可能比较容易混淆:这里讲述的解析与分派这两者之间的关系并不是二选一的排他关系,它们是在不同层次上去筛选和确定目标方法的过程。例如,前面说过静态方法会在类加载期就进行解析,而静态方法显然也是可以拥有重载版本的,选择重载版本的过程是通过静态分派完成的。

2. 动态分派

了解了静态分派,我们接下来看一下动态分派的过程,它和多态性的另外一个重要体现 一 重写(Override) 有着很密切的关联,我们看下示例代码

/*
方法动态分派演示
*/
public class Test {

    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 Women extends Human{
        @Override
        protected void sayHello() {
            System.out.println("women say hello");
        }
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human women = new Women();
        man.sayHello();
        women.sayHello();
        man = new Women();
        man.sayHello();
    }
}
/*
man say hello
women say hello
women say hello
*/

通过结果我们可以看出来,这里不可能根据静态类型来决定,这里试通过实际类型来区分的,我们通过javap来查看下字节码

javap -verbose Test

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class Test$Man
         3: dup
         4: invokespecial #3                  // Method Test$Man."<init>":()V
         7: astore_1
         8: new           #4                  // class Test$Women
        11: dup
        12: invokespecial #5                  // Method Test$Women."<init>":()V
        15: astore_2
        16: aload_1
        17: invokevirtual #6                  // Method Test$Human.sayHello:()V
        20: aload_2
        21: invokevirtual #6                  // Method Test$Human.sayHello:()V
        24: new           #4                  // class Test$Women
        27: dup
        28: invokespecial #5                  // Method Test$Women."<init>":()V
        31: astore_1
        32: aload_1
        33: invokevirtual #6                  // Method Test$Human.sayHello:()V
        36: return

0~15行的字节码是准备动作,作用是建立man和women的内存空间、调用Man和Womn类型的实例构造器,将这两个实例的引用存放在第1和第2个局部变量表Slot之中,这个动作对应了代码中的这两句:

 Human man = new Man();
 Human women = new Women();

接下来的第16~21行是关键部分,第16和第20两行分别把刚刚创建的两个 对象的引用压到栈顶,这两个对象是将要执行的sayHello方法的所有者,称为 接收者(Receiver);第17和第21两行是方法调用指令,单从字节码的角度来看,这两条调用指令无论是指令(都是invokevirtual)还是参数(都是常量池中第22项的常量,注释显示了这个常量是Human.sayHello的符号引用)都完全一样,但是这两条指令最终执行的目标方法并不相同,其原因需要从invokevirtual指令的多态查找过程开始说起,invokevirtual指令的运行时解析过程大致分为以下步骤:

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

3. 单分派与多分派

方法的接收者与方法的参数统称为 方法的宗量,这个定义最早应该来源于《Java与模式》一书的译文。根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个的宗量对目标方法进行选择。

字面看起来不太直观,我们可以通过代码演示 “一个艰难的决定“ 来看这两个定义

/*
单分派,多分派的演示
*/
public class Test {

    static class QQ{}
    static class _360{}

    public static class Father{
        public void hardChoice(QQ arg){
            System.out.println("father choose qq");
        }
        public void hardChoice(_360 arg){
            System.out.println("father choose 360");
        }
    }

    public static class Son extends Father{
        public void hardChoice(QQ arg){
            System.out.println("son choose qq");
        }
        public void hardChoice(_360 arg){
            System.out.println("son choose 360");
        }
    }

    public static void main(String[] args) {
        Father father = new Father();
        Father son = new Son();
        father.hardChoice(new _360());
        son.hardChoice(new QQ());
    }
}
/**
father choose 360
son choose qq
*/

来看看编译阶段编译器的选择过程,即静态分派的过程。这时候选择目标方法的依据有两点:一是静态类型是Father还是Son,二是方法参数是QQ还是360。这次选择结果的最终产物是产生了两条invokevirtual指令,两条指令的参数分别为常量池中指向Father.hardChoice(360)Father.hardChoice(QQ)方法的符号引用。因为是 根据两个宗量进行选择,所以Java语言的静态分派属于多分派类型

再看看运行阶段虚拟机的选择,即动态分派的过程。在执行son.hardChoice(newQQO)这句代码时,更准确地说,在执行这句代码所对应的invokevirtual指令时,由于编译期已经决定目标方法的签名必须为hardChoice(QQ),虚拟机此时不会关心传递过来的参数“QQ”到底是“腾讯QQ”还是“奇瑞QQ”,因为这时候参数的静态类型、实际类型都不会对方法的选择构成任何影响,唯一可以影响虚拟机选择的因素只有此方法的接收者的实际类型是Father还是Son。因为只有一个宗量作为选择依据,所以Java语言的动态分派属于单分派类型

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

由于 动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因此在虚拟机的实际实现中基于性能的考虑,大部分实现都不会真的进行如此频繁的搜索。面对这种情况,最常用的“稳定优化”手段就是为类在方法区中建立一个 虚方法表(Virtual Method Table,也称为vtable,与此对应,在invokeinterface执行时也会用到接口方法表一Interface Method Table,简称itable),使用虚方法表索引来代替元数据查找以提高性能。我们先看看前面代码所对应的虚方法表结构示例:

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/44991.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【LeetCode每日一题】——771.宝石与石头

文章目录一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【解题思路】七【题目提示】八【时间频度】九【代码实现】十【提交结果】一【题目类别】 字符串 二【题目难度】 简单 三【题目编号】 771.宝石与石头 四【题目描述】 给你一个字符串…

配电网电压调节及通信联系研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️❤️&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清…

三年城市NOH落地100城,毫末智行内部信剑指2025

11月29日&#xff0c;毫末智行董事长张凯、CEO顾维灏联合发布《毫末智行三周岁&#xff1a;三年磨一剑 利刃开新篇》的内部信&#xff0c;提到毫末愿景及战略目标&#xff1a;“让机器智能移动&#xff0c;给生活更多美好。”未来成长为一家产品矩阵覆盖全无人驾驶、机器人等多…

[附源码]Python计算机毕业设计Django“科教兴国”支教门户网站

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

【毕业设计】26-基于单片机心跳体温血压系统仿真设计(原理图+仿真+演示视频+论文)

【毕业设计】基于单片机心跳体温血压系统仿真设计&#xff08;原理图仿真演示视频论文&#xff09; 文章目录【毕业设计】基于单片机心跳体温血压系统仿真设计&#xff08;原理图仿真演示视频论文&#xff09;任务书设计说明书摘要设计说明书及设计文件任务书 以单片机为控制核…

DPDK helloworld示例程序

目录 helloworld源代码 helloword编译 helloworld代码解析 DPDK的helloworld示例程序&#xff0c;用以示例DPDK应用程序的编写和使用。 helloworld源代码 helloworld代码构成&#xff1a; /* 省略系统头文件*//* DPDK相关的rte头文件 */ #include <rte_memory.h>…

springboot 集成JWT实现token验证

JWT介绍 是一种开放标准 &#xff08;RFC 7519&#xff09;&#xff0c;它定义了一种紧凑且独立的方式&#xff0c;用于将信息作为 JSON 对象在各方之间安全地传输。该信息可以进行验证和信任&#xff0c;因为它是经过数字签名的。JWT 可以使用密钥&#xff08;使用 HMAC 算法…

拾忆书苑(图书商城系统)网站的设计与实现(html;DIV+CSS; Bootstrap; Dreamweaver; Photoshop)

目 录 一、绪论 1 &#xff08;一&#xff09;课题研究背景 1 &#xff08;二&#xff09;课题研究目的及意义 1 二、相关技术与工具介绍 1 &#xff08;一&#xff09;Dreamweaver开发技术 1 &#xff08;二&#xff09;Adobe Photoshop 1 三、拾忆书店网站的设计与分析 2 &am…

某村庄供水工程设计(设计报告+cad图纸+预算工程量清单)

目 录 工程特性表 1 1 综合说明 3 1.1 工程背景 3 1.2 设计依据 3 1.3 工程建设的必要性与可行性 3 1.4 供水范围、规模及水源选择 4 1.5 工程总体布置 4 1.6 工程设计 4 1.7 工程施工 5 1.8 工程管理 5 1.9 预算 5 2 项目区概况及项目建设的必要性 6 2.1 项目区自然概况 6 2.2…

Head First设计模式(阅读笔记)-06.命令模式

家电自动化遥控器 假设要创建一个遥控器&#xff0c;该遥控器上有7个插槽(每个可以插上不同的家电)&#xff0c;每个插槽对应了开关按钮&#xff0c;并且遥控器上还有一个撤销按钮用于撤销上一次的操作 从餐厅点餐开始 假设一个顾客来到餐厅要进行点餐&#xff0c;整体流程如下…

不用ps怎么修改图片?电脑图片在线处理的方法

很多小伙伴在工作、学习的时候会用到简单的图片处理工具&#xff08;在线ps 图片编辑制作工具 免费照片编辑器_压缩图&#xff09;&#xff0c;但是因为用到的功能比较多&#xff0c;需要下载安装很多电脑处理图片软件&#xff0c;非常不方便&#xff0c;而且上手比较难。下面就…

【学习笔记53】JavaScript正则表达式练习题

正则表达式练习题一、用户名、密码和手机号的验证1、案例要求2、案例分析3、HTML和CSS代码4、JS代码二、密码强度1、案例要求2、案例分析3、HTML和CSS代码4、JS代码的实现三、书写正则验证邮箱1、邮箱的验证2、代码的实现四、书写正则验证0~255的数字一、用户名、密码和手机号的…

global关键字、python实现ATM简单功能

目录 一.局部变量、全局变量 二.global关键字 演示 三.编写ATM程序 要求 详细步骤 存在问题 改进 完整代码 一.局部变量、全局变量 1.什么是局部变量 作用范围在函数内部&#xff0c;在函数外部无法使用 2.什么是全局变量 在函数内部和外部均可使用 3.如何将函数内定…

Nature子刊:精准预测分子性质和药物靶标的无监督学习框架

药品的临床疗效与安全性由在人类蛋白质组内的分子靶标决定。本文中&#xff0c;湖南大学信息科学与工程学院的李肯立/曾湘祥教授课题组提出了一种无监督的预训练深度学习框架&#xff0c;对 1000 万个未标记的类药性、生物活性分子进行预训练&#xff0c;以预测候选化合物的药物…

osgEarth示例分析——osgearth_manip

前言 本示例主要演示osgEarth的事件处理的用法&#xff0c;内容比较多&#xff0c;这部分功能也很重要。 输入命令依然采用china-simple.earth的示例&#xff0c;加上了模型&#xff0c;但是模型并没有看到&#xff0c;可能是因为模型没有放大太小的原因。在代码中设置了不加…

传奇单机架设登录器配置教程

传奇单机顾名思义就是在本地电脑上架设传奇&#xff0c;限制同一个局域网才能一起玩&#xff0c;我接触到几个朋友不明白外网和单机的区别 架设单机需要准备以下程序&#xff1a; 传奇服务端&#xff08;版本Mirserver&#xff09; DBC2000 (百度可直接下载&#xff09; 配套登…

IPv6进阶:IPv6 过渡技术之 GRE 隧道

实验拓扑 R1-R3-R2之间的网络为IPv4环境PC1及PC2处于IPv6孤岛 实验需求 R1及R2为IPv6/IPv4双栈设备在R1及R2上部署GRE隧道使得PC1及PC2能够互相访问&#xff08;先采用IPv6静态路由实现互通&#xff09;R1及R2基于建立好的GRE隧道运行OSPFv3交互IPv6路由前缀 实验步骤及配置…

【数据可视化】免费开源BI工具 DataEase 之 Tab 组件前世今生

小D &#xff1a;小助理&#xff0c;小助理&#xff0c;在哪儿呢&#xff1f;&#xff08;焦急脸&#xff09; BI 小助理&#xff1a;在呢&#xff0c;啥事&#xff1f;&#xff08;不耐烦脸&#xff09; 小D &#xff1a;又有 BI 工具放大招啦&#xff01;&#xff01;&…

C语言习题练习11--指针

1.代码结果 #include <stdio.h> int main() {int arr[] {1,2,3,4,5};short *p (short*)arr;int i 0;for(i0; i<4; i){*(pi) 0;}for(i0; i<5; i){printf("%d ", arr[i]);}return 0; } 正常&#xff1a;0001--00 02--00 03--00 04--00 05 数组内部是倒…

Kotlin高仿微信-第2篇-登录

Kotlin高仿微信-项目实践58篇详细讲解了各个功能点&#xff0c;包括&#xff1a;注册、登录、主页、单聊(文本、表情、语音、图片、小视频、视频通话、语音通话、红包、转账)、群聊、个人信息、朋友圈、支付服务、扫一扫、搜索好友、添加好友、开通VIP等众多功能。 Kotlin高仿…