十、方法调用的底层实现

一、方法调用分析(main方法是JVM指令执行的起点)

                我们写的代码,经过编译、经过类加载的各种阶段,进入了 JVM 的运行时数据区。但作为程序员真正关心是代码的执行,代码的执行其实本质上是方法的执行,站在 JVM 的角度归根到底还是字节码的执行。

main 函数是 JVM 指令执行的起点,JVM 会创建 main 线程来执行 main 函数,以触发 JVM 一系列指令的执行,真正地把 JVM 跑起来。

        接着,在我们的代码中,就是方法调用方法的过程,所以了解方法在 JVM 中的调用是非常必要的。

方法的调用与虚拟机栈示意图

二、方法调用的字节码指令

关于方法的调用,Java 字节码提供了 5 个指令,来调用不同类型的方法:

  • invokestatic 用来调用静态方法;--非虚方法
  • invokespecial 用于调用私有实例方法、构造器及 super 关键字等;--非虚方法
  • invokevirtual 用于调用非私有实例方法,比如 public 和 protected,大多数方法调用属于这一种;-- 虚方法
  • invokeinterface 和上面这条指令类似,不过作用于接口类; -- 虚方法
  • invokedynamic 用于调用动态方法。

三、非虚方法

        如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称为非虚方法。这类方法的调用称为解析。它们在类加载的时候就会把符号引用解析为该方法的直接引用。

        静态方法,在编译的时候就已经写入到了常量池--非虚方法

        只要能被 invokestatic 和 invokespecial 指令调用的方法,都可以在解析阶段中确定唯一的调用版本,Java 语言里符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法 4 种,再加上被 final 修饰的方法(尽管它使用 invokevirtual 指令调用),这 5 种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用。不需要在运行时再去完成。

  • invokeStatic 用来调用静态方法
public static void main(String[] args) StaticResolution.Hello();
}

javap -v

这个方法调用在编译期间就明确以常量池项的形式固化在字节码指令的参数之中了。

  • invokeSpecial 用于调用私有实例方法、构造器及 super 关键字等

四、虚方法

        与非虚方法相反,不是非虚方法的方法就是虚方法。主要包括以下字节码中的两类

  • invokevirtual 用于调用非私有实例方法,比如 public 和 protected,大多数方法调用属于这一种(排除掉被 final 修饰的方法);
  • invokeinterface 和上面这条指令类似,不过作用于接口类;

        为什么叫做虚方法呢?就是方法在运行时是可变的。

        很多时候,JVM 需要根据调用者的动态类型,来确定调用的目标方法,这就是动态绑定的过程;相对比,invokestatic 指令和 invokespecial 指令,就属于静态绑定过程。

        因为 invokeinterface 指令跟 invokevirtual 类似,只是作用与接口,所以我们只要熟悉 invokevirtual 即可。

        方法调用并不等于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。

        在程序运行时,进行方法调用是最普遍、最频繁的操作,但是Class文件的编译过程不包括传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相对于之前说的直接引用)。这个特性给Java带来了更强大的动态扩展能力,但也使得Java方法调用过程变得相对复杂起来,需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。

  • 解析

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

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

        静态方法、私有方法、实例构造器、父类方法。这些方法称为非虚方法,它们在类加载的时候就会把符号引用解析为该方法的直接引用。与之相反,其他方法称为虚方法(除去final方法)

分派

  • 多态就是动态分派和静态分派

        方法会根据你送入的参数有不同的表现形式,这个就是分派。

要了解虚方法我们必须了解以下基础:

        Java 是一门面向对象的程序语言,因为 Java 具备面向对象的 3 个基本特征:继承、封装和多态。

        分派调用过程将会揭示多态性特征的一些最基本的体现,如“重载”和“重写”在 Java 虚拟机之中是如何实现的

  • 静态分派-重载

        多见于方法的重载。(重载:一个类中允许同时存在一个以上的同名方法,这些方法的参数个数或者类型不同)

重载使用 invokevirtual 指令

        “Human”称为变量的静态类型(Static Type),或者叫做的外观类型(Apparent Type),后面的“Man”则称为变量的实际类型(Actual Type)。

        静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。父类引用指向子类对象,编译并不知道实际类型,只有运行的时候才知道。

        代码中定义了两个静态类型相同但实际类型不同的变量,但虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译期可知的。因此,在编译阶段,Javac 编译器会根据参数的静态类型决定使用哪个重载版本,所以选择了 sayHello(Human)作为调用目标。

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。

       静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的,而是由编译器来完成。

所以代码运行结果如下:

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,gentlemen!");}public static void sayHello(Woman guy){System.out.println("hello,lady!");}public static void main(String[] args) {Human man=new Man();Human woman=new Woman();sayHello(man);sayHello(woman);}
}

输出:

hello,guy!

hello,guy!

总结:例子很简单,方法会根据你送入的参数有不同的表现形式,这个就是分派。

        举个简单的例子:你在酒吧遇到一个你心动的人,但这个人看上去不男不女,你怎么去与他/她打招呼?这个时候我至少知道是一个人,所以 打招呼说:地球人!你好,我是来自火星的(hello guy!)。我调用它最原始的外观类型(至少是个人)

  • 动态分派-重写

多见于方法的重写。(重写:在子类中将父类的成员方法的名称保留,重新编写成员方法的实现内容,更改方法的访问权限,修改返回类型的为父类返回类型的子类。)

另外一个例子:

public class DynamicDispatch {static abstract class Human{protected abstract void sayHello();}static class Man extends Human{ @Overrideprotected void sayHello() { System.out.println("man say hello!");}}static class Woman extends Human{ @Overrideprotected void sayHello() { System.out.println("woman say hello!");}} public static void main(String[] args) {Human man=new Man();Human woman=new Woman();man.sayHello();woman.sayHello();man=new Woman();man.sayHello(); }
}

输出:

man say hello!

woman say hello!

woman say hello!

重写也是使用 invokevirtual 指令,只是这个时候具备多态性。

        显然,这里不可能再根据静态类型来决定,因为静态类型同样是Human的两个变量man和woman在调用sayHello()方法时执行了不同的行为,并且变量man在两次调用中执行了不同的方法。导致这个现象的原因很明显,是这两个变量的实际类型不同,Java虚拟机是如何根据实际类型来分派方法执行版本的呢

invokevirtual 指令有多态查找的机制,该指令运行时,解析过程如下:

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

        由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言方法重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。

        另外一点,这个时候结合之前课程中讲过虚拟机栈中栈中的内容,我就知道动态链接是干嘛的:

        invokevirtual 可以知道方法 call()的符号引用转换是在运行时期完成的,在方法调用的时候。部分符号引用在运行期间转化为直接引用,这种转化就是动态链接。

详细看栈帧执行对内存区域的影响

方法表,也叫虚方法表

        虚拟机动态分派的实现

        动态分派会执行非常频繁的动作,JVM 运行时会频繁的、反复的去搜索元数据,所以 JVM 使用了一种优化手段,这个就是在方法区中建立一个虚方法表。使用虚方法表索引来替代元数据查找以提高性能。

        在实现上,最常用的手段就是为类在方法区中建立一个虚方法表。虚方法表中存放着各个方法的实际入口地址。

        如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。

        如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。

        如图中,Son 重写了来自 Father 的全部方法,因此 Son 的方法表没有指向 Father 类型数据的箭头。

        但是 Son 和 Father都没有重写来自 Object 的方法,所以它们的方法表中所有从 Object 继承来的方法都指向了 Object 的数据类型。

        方法表一般在类加载阶段的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕。

五、接口调用

  • invokeinterface 和 invokevirtual 指令类似,不过作用于接口类的default方法

六、Lambda 表达式

  • invokedynamic,用于调用动态方法

lambda表达式

Runnable r = () -> System.out.println("Hello Lambda!");

        主要使用invokedynamic,用于调用动态方法。底层是 methodHandle,方法句柄

        methodhandle 比反射好用,效率高,但是反射权限更大,能看到更多底层

        invokedynamic 这个字节码是比较复杂。和反射类似,它用于一些动态的调用场景,但它和反射有着本质的不同,效率也比反射要高得多。

        invokedynamic这个指令通常在 Lambda 语法中出现,我们来看一下一小段代码:

        使用 javap -v 命令可以在 main 方法中看到 invokedynamic 指令:

        另外,我们在 javap 的输出中找到了一些奇怪的东西:

        BootstrapMethods 属性在 Java 1.7 以后才有,位于类文件的属性列表中,这个属性用于保存 invokedynamic 指令引用的引导方法限定符。和上面介绍的四个指令不同,invokedynamic 并没有确切的接受对象,取而代之的,是一个叫 CallSite 的对象。

七、方法句柄-MethodHandler

官方文档解释:https://docs.oracle.com/javase/7/docs/api/java/lang/invoke/MethodHandles.html

        invokedynamic 指令的底层,是使用方法句柄(MethodHandle)来实现的。方法句柄是一个能够被执行的引用,它可以指向静态方法和实例方法,以及虚构的 get 和 set 方法,从以下案例中可以看到 MethodHandle 提供的一些方法。

        MethodHandle 是什么?简单的说就是方法句柄,通过这个句柄可以调用相应的方法。

用 MethodHandle 调用方法的流程为:

(1) 创建 MethodType,获取指定方法的签名(出参和入参)

(2) 在 Lookup 中查找 MethodType 的方法句柄 MethodHandle

(3) 传入方法参数通过 MethodHandle 调用方法

methodhandle 比反射好用,效率高,但是反射权限更大,能看到更多底层

MethodType

MethodType 表示一个方法类型的对象,每个 MethodHandle 都有一个 MethodType 实例,MethodType 用来指明方法的返回类型和参数类型。其有多个工厂方法的重载。

STATIC METHODTYPE METHODTYPE(CLASS RTYPE)

STATIC METHODTYPE METHODTYPE(CLASS RTYPE, CLASS PTYPE0)

STATIC METHODTYPE METHODTYPE(CLASS RTYPE, CLASS[] PTYPES)

STATIC METHODTYPE METHODTYPE(CLASS RTYPE, CLASS PTYPE0, CLASS... PTYPES)

STATIC METHODTYPE METHODTYPE(CLASS RTYPE, LISTLASS> PTYPES)

STATIC METHODTYPE METHODTYPE(CLASS RTYPE, METHODTYPE PTYPES)

Lookup

MethodHandle.Lookup 可以通过相应的 findxxx 方法得到相应的 MethodHandle,相当于 MethodHandle 的工厂方法。查找对象上的工厂方法对应于方法、构造函数和字段的所有主要用例。

findStatic 相当于得到的是一个 static 方法的句柄(类似于 invokestatic 的作用)

findVirtual 找的是普通方法(类似于 invokevirtual 的作用)

invoke

其中需要注意的是 invoke 和 invokeExact,前者在调用的时候可以进行返回值和参数的类型转换工作,而后者是精确匹配的。

所以一般在使用是,往往 invoke 使用比 invokeExact 要多,因为 invokeExact 如果类型不匹配,则会抛错。

Lambda 表达式的捕获与非捕获

当 Lambda 表达式访问一个定义在 Lambda 表达式体外的非静态变量或者对象时,这个 Lambda 表达式称为“捕获的”

那么“非捕获”的 Lambda 表达式来就是 Lambda 表达式没有访问一个定义在 Lambda 表达式体外的非静态变量或者对象

Lambda 表达式是否是捕获的和性能悄然相关。一个非捕获的 lambda 通常比捕获的更高效,非捕获的 lambda 只需要计算一次. 然后每次使用到它都会返回一个唯一的实例。而捕获的 lambda 表达式每次使用时都需要重新计算一次,而且从目前实现来看,它很像实例化一个匿名内部类的实例。

lambda 最差的情况性能内部类一样, 好的情况肯定比内部类性能高。

Oracle 公司的性能比较的文档,详细而全面的比较了 lambda 表达式和匿名函数之间的性能差别。

lambda 开发组也有一篇 PPT 其中也讲到了 lambda 的性能(包括 capture 和非 capture 的情况)。 lambda 最差的情况性能内部类一样, 好的情况肯定比内部类性能高。

https://www.oracle.com/technetwork/java/jvmls2013kuksen-2014088.pdf

http://nerds-central.blogspot.tw/2013/03/java-8-lambdas-they-are-fast-very-fast.html

/*** 方法句柄(MethodHandle)使用案例**/
public class MethodHandleDemo {static class Bike {String sound(Integer a) {return "ding  ding ding";}}static class Animal {String sound(Integer a) {return "wow  wow wow";}}static class Man extends Animal {@OverrideString sound(Integer a) {return "ha ha ha" + a;}}String sound(Object o) throws Throwable {// 1、方法句柄--工厂方法FactoryMethodHandles.Lookup lookup = MethodHandles.lookup();// 2、方法类型表示接受的参数和返回类型(第一个参数是返回参数)MethodType methodType = MethodType.methodType(String.class,Integer.class);// 3、拿到具体的MethodHandle(findVirtual相当于字节码)MethodHandle methodHandle = lookup.findVirtual(o.getClass(), "sound", methodType);// 4、执行方法String obj = (String) methodHandle.invoke(o,1);return obj;}public static void main(String[] args) throws Throwable {// 每次送入的实例不一样String str = new MethodHandleDemo().sound(new Bike());System.out.println(str);str = new MethodHandleDemo().sound(new Animal());System.out.println(str);str = new MethodHandleDemo().sound(new Man());System.out.println(str);}
}

总结

        Lambda 语言实际上是通过methodhanle方法句柄来完成的,大致这么实现(JVM 编译的时候使用 invokedynamic 实现 Lambda 表达式,invokedynamic的是使用 MethodHandle 实现的,所以 JVM 会根据你编写的 Lambda 表达式的代码,编译出一套可以去调用 MethodHandle 的字节码代码,参考实例类:MethodHandleDemo)

        句柄类型(MethodType)是我们对方法的具体描述,配合方法名称,能够定位到一类函数。访问方法句柄和调用原来的指令基本一致,但它的调用异常,包括一些权限检查,在运行时才能被发现。

        案例中,我们完成了动态语言的特性,通过方法名称和传入的对象主体,进行不同的调用,而 Bike 和 Man 类,可以没有任何关系。

        可以看到 Lambda 语言实际上是通过方法句柄来完成的,在调用链上自然也多了一些调用步骤,那么在性能上,是否就意味着 Lambda 性能低呢?对于大部分“非捕获”的 Lambda 表达式来说,JIT 编译器的逃逸分析能够优化这部分差异,性能和传统方式无异;但对于“捕获型”的表达式来说,则需要通过方法句柄,不断地生成适配器,性能自然就低了很多(不过和便捷性相比,一丁点性能损失是可接受的)。

        invokedynamic 指令,它实际上是通过方法句柄来实现的。和我们关系最大的就是 Lambda 语法,我们了解原理,可以忽略那些对 Lambda 性能高低的争论,同时还是要尽量写一些“非捕获”的 Lambda 表达式。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

6、LLaVA

简介 LLaVA官网 LLaVA使用Vicuna(LLaMA-2)作为LLM f ϕ ( ⋅ ) f_\phi() fϕ​(⋅),使用预训练的CLIP图像编码器 ViT-L/14 g ( X v ) g(X_v) g(Xv​)。 输入图像 X v X_v Xv​,首先获取feature Z v g ( X v ) Z_vg(X_v) Zv​g(Xv​)。考虑到最后一…

WPF+Halcon 培训项目实战(8):WPF+Halcon初次开发

前言 为了更好地去学习WPFHalcon,我决定去报个班学一下。原因无非是想换个工作。相关的教学视频来源于下方的Up主的提供的教程。这里只做笔记分享,想要源码或者教学视频可以和他联系一下。 相关链接 微软系列技术教程 WPF 年度公益课程 Halcon开发 CSD…

MAC运行Windows专用软件 CrossOver v23.7.1中文版 macOS

CrossOver v23.7.1中文版是一款系统兼容软件,让您可以在 Mac 和 Linux 系统上运行 Windows 应用,不必购买 Windows 授权,不必重启系统,不必使用虚拟机。通过 CrossOver, 您可以从 dock 直接启动 Windows 应用&#xff…

Android笔记(二十三):Paging3分页加载库结合Compose的实现分层数据源访问

在Android笔记(二十二):Paging3分页加载库结合Compose的实现网络单一数据源访问一文中,实现了单一数据源的访问。在实际运行中,往往希望不是单纯地访问网络数据,更希望将访问的网络数据保存到移动终端的SQL…

pytest pytest-emoji通过表情包展示执行状态

pytest-emoji 是一个用于在 Pytest 测试运行期间显示 emoji 表情的插件。它可以为测试结果添加一些有趣的表情符号,以增加测试报告的可读性和趣味性。 使用 pytest-emoji 插件非常简单,只需按照以下步骤进行操作: 首先,确保已经安…

【数据结构】快速排序(4种方式实现)

前言:前面我们学习了几种相对比较简单的排序,今天我们要一起学习的是快速排序,我们将通过四种方式来模拟实现快排。 💖 博主CSDN主页:卫卫卫的个人主页 💞 👉 专栏分类:数据结构 👈 &#x1f4a…

K-means 聚类算法分析

算法简述 K-means 算法原理 我们假定给定数据样本 X ,包含了 n 个对象 ,其中每一个对象都具有 m 个维度的属性。而 K-means 算法的目标就是将 n 个对象依据对象间的相似性聚集到指定的 k 个类簇中,每个对象属于且仅属于一个其到类簇中心距离…

基于MINIST的手写数字体识别

一、算法简述 网络结构设计 通过创建MnistNet类,定义了包含两个卷积层和两个全连接层的深度神经网络。这个网络的设计灵感来自于经典的CNN结构,其中卷积层用于提取图像特征,而全连接层则用于将这些特征映射到最终的类别。 卷积与池化 卷…

js对象方法大全(开发必会)

目录 前言 assgin(对象合并) 参数 功能 返回值 测试 结果 结论 create(以源对象为原型创建新对象) 参数 功能 返回值 测试 结果 结论 defineProperties(对属性进行具体定义) 参数 功能 返回值 测试 结果 结论 defineProperty(重写或定义新属性) 参数 功…

【Hive_04】分区分桶表以及文件格式

1、分区表1.1 分区表基本语法(1)创建分区表(2)分区表读写数据(3)分区表基本操作 1.2 二级分区1.3 动态分区 2、分桶表2.1 分桶表的基本语法2.2 分桶排序表 3、文件格式与压缩3.1 Hadoop压缩概述3.2 Hive文件…

用轻量级ORM--Dapper实现泛型仓储

阅读本文你的收获 了解Dapper的适用场景了解Dapper的本质其实是一些扩展方法学会使用Dapper的扩展Domel来实现泛型仓储 一、什么是Dapper? Dapper是一个轻量级的ORM(对象关系映射)工具,用于简化数据库操作。它和Entity Framewor…

Java 类加载与字节码技术

3 类加载与字节码技术 3.1 类文件结构 类文件结构字节码指令编译期处理类加载阶段类加载器运行期优化 根据 JVM 规范,类文件结构如下 ClassFile {u4 magic;u2 minor_version; // 小版本号u2 major_version; // 主版本号u2 constant_pool_count; // 常量池cp_info…