zoukankan      html  css  js  c++  java
  • JVM方法调用

    JVM的静态绑定和动态绑定

    JVM识别方法的关键在于类名、方法名及方法描述符(method descriptor)。方法描述符是由方法的参数类型和返回类型所构成。在同一个类中,如果同时出现多个类名方法名以及描述符都相同的方法,java虚拟机会在类的验证阶段报错。

    java虚拟机与java语言不同,JVM不限制方法名和参数类型相同,返回类型不同的方法出现在同一个类中,对于调用这些方法的字节码来说,由于字节码所附带的方法描述符包含了返回类型,因此java虚拟机能够准确的识别目标方法。

    java虚拟机的静态绑定:指的是在解析时便能够直接识别目标方法的情况。即在编译时期解析,指令指向的方法就是静态方法,也就是private、final、static和构造方法。

    java虚拟机的动态绑定:指的是需要在运行过程中根据调用者的动态类型来识别目标方法的情况,比如接口和虚方法调用无法找到真正需要调用的方法,因为它可能是定义在子类中的方法,所以这种在运行时期再能明确类型的方法我们成为动态绑定。

    JVM提供了如下方法调用指令:

    1、invokestatic: 调用静态方法。

    2、invokespecial: 调用私有实例方法、构造器,以及使用super关键字调用父类的实例方法或构造器,和所有实现接口的默认方法。

    3、invokevirtual: 调用虚方法即非私有的实例方法。

    4、invokeinterface: 调用接口方法,在运行时再确定一个实现此接口的对象。

    5、invokedynamic: 调用动态方法,在运行时动态解析出调用点限定符所引用的方法之后,调用该方法。

    调用指令的符号引用

    在编译过程中,我们并不知道目标方法的具体内存地址。因此,java编译器会暂时用符号应用来表示该目标方法。这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名或方法描述符。

    符号应用存储在class文件的常量池中。根据目标方法是否为接口。这些引用可分为接口符号引用和非接口符号引用。在执行符号引用的字节码前,java虚拟机需要解析这些符号引用,并替换为实际引用。

    虚方法的调用

    java虚拟机中的虚方法调用:

    1、java里所有非私有的实例方法调用编译成invokevirtual指令

    2、接口方法调用被编译成invokeinterface指令

    在绝大多数情况下,JVM需要根据调用者的动态类型,来确定虚方法调用的目标方法。这个过程我们称之为动态绑定。性对于静态绑定的非虚方法调用来说,虚方法调用更加耗时。

    在JVM中采用空间换时间的策略来实现动态绑定,他为每个类生成一张方法表,用以快速定位目标方法。

    方法表

    类加载的准备阶段,除了为静态字段分配内存外,还会构造与该类相关联的方法表。

    这个数据结构是JVM实现动态绑定的关键所在。

    方法表本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法。这些方法可能是具体的可执行的方法,也可能是没有响应字节码的抽象方法。

    方法表满足两个特质:

    1、子类方法表中包含父类方法表中的所有方法。

    2、子类方法在方法表中的索引值,与他所重写的父类方法的索引值相同。

    方法调用指令中的符号引用会在执行之前解析成实际引用。对于静态绑定的方法调用而言,实际引用指向具体的目标方法。对于动态绑定的方法而言,实际引用则是方法表中的索引值(实际并不仅是索引值)。

    在执行过程中,JVM将获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获取目标方法。这个过程便是动态绑定。

    实际上,使用了方法表的动态绑定与静态绑定相比,仅仅多出了几个内存解引用操作:访问栈上的调用者,读取调用者的动态类型,读取该类型的方法表,读取方法表中某个索引值对应的目标方法。相对于创建并初始化java栈帧来说,这几个内存解引用操作的开销可以忽略不计。

    但是,虚方法调用对性能仍然有影响:

    方法表的引入带来的优化效果仅存在与解释执行或者即时编译代码的最坏情况下,而且即时编译还拥有两个性能更好的优化手段:内联缓存(inlining cache)和方法内联(method inlining)。

    内联缓存(inlining cache)

    内联缓存是一种加快动态绑定的优化技术。它能够缓存虚方法调用中调用者的动态类型,以及该类型所对应的目标方法。后续执行中,优先使用缓存,没有缓存则使用方发表。

     多态优化相关的的术语:

    1、单态(monomorphic):指的是仅有一种状态的情况。

    2、多态(polymorphic):指的是有限数量种状态的情况,二态是多态的一种。

    3、超多态(megamorphic):指的是更多种状态的情况。通常用某个阈值来区分多态和超多态。

    所以,内联缓存也有对应的单态内联缓存、多态内联缓存、超多态内联缓存。

    单态内联缓存:即只缓存了一种动态类型及所对应的目标方法。他的实现比较简单,即比较所缓存的动态类型,如果命令则直接调用对应的目标方法。

    多态内联缓存:则缓存了多个动态类型及所对应的目标方法。它的实现需要逐个将所缓存的动态类型与当前动态类型进行比较,如果命中,则调用所对应的动态方法。

    一般来说,我们将更加热门的动态类型放在前面。在实践中大部分的虚方法调用都是单态的,也就是只有一种动态类型。为了节省内存空间,JVM只采用单态内联缓存。

    在选择内联缓存时,如果未命中则重新使用方发表做动态绑定。这时有两种选择:

    1、替换单态内联缓存中的记录:

    这种做法就好比 CPU 中的数据缓存,他对数据的局部性有要求,即在替换内联缓存之后的一段时间内,方法调用的调用者的动态类型要保持一致,从而能够有效地利用内联缓存。在最坏的情况下,用两种不同类型的调用者,轮流的执行该方法调用,那么每次进行执行该方法调用都要替换内联缓存。也就是说,只有写缓存的额外开销,没有用缓存的性能提升。

    2、劣化为超多态状态

     这也是JVM的具体实现方式。处于这种状态下的内联缓存,实际上放弃了优化的机会。它将直接访问方法表,来动态绑定目标方法。与替换内联缓存记录的做法相比,它牺牲了优化的机会,但节省了写缓存的额外开销。

    虽然内联缓存附带了内联二字,但是他并没有内联目标方法。这里需要明确的是,任何方法调用,除非被内联,否则都会有固定开销。这些开销来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。

    invokedynamic

     该指令的调用机制抽象出调用点这一个概念,并允许应用程序将调用点链接至任意符合条件的方法上。

    作为invokedynamic的准备工作,java7引入了更加底层、更加灵活地抽象方法:方法句柄。

    方法句柄:是一个强类型的,能够被直接执行的引用。该引用可以指向常规的静态方法或者实例方法,也可以指向构造器或者字段。当指向字段时,方法句柄实则指向包含字段访问字节码的虚构方法,语义上等价于目标字段的getter/setter方法。但是它并不会直接指向目标字段所在类中的getter/setter,毕竟你无法保证已有的getter/setter方法就是在访问目标字段。

    方法句柄的类型是由所指向方法的参数类型及返回类型组成的。它是用来确认方法句柄是否适配的唯一关键。当时用方法句柄时其实我们并不关心方法句柄所指向方法的方法名或者类名。

    方法句柄的创建是通过MethodHandles.Lookup类来完成的。它提供了多个API,既可以使用反射API中的Method来查找,也可以根据类、方法名以及方法句柄的类型来查找。

    注:此文为极客时间郑雨迪专栏,java虚拟机讲解及自己查资料的学习总结。郑雨迪《深入拆解Java虚拟机》很不错。

  • 相关阅读:
    java总结2
    java总结
    java动手动脑
    今日代码总结
    JavaScript 中 几 个需要掌握基础的问题
    JavaScript中如何将指定的某个字符全部转换为其他字符
    HTML页面一键分享到QQ空间、QQ好友、新浪微博、微信代码
    jq动画里这样写css属性
    h5 前端面试题
    ES6 object.defineProperty
  • 原文地址:https://www.cnblogs.com/cathyqq/p/9503052.html
Copyright © 2011-2022 走看看