前不久,“虚拟机”赛马俱乐部来了个年轻人,标榜自己是动态语言,是先进分子。

这一天,先进分子牵着一头鹿进来,说要参加赛马。咱部里的老学究 Java 就不同意了呀,鹿又不是马,哪能参加赛马。

当然了,这种墨守成规的调用方式,自然是先进分子所不齿的。现在年轻人里流行的是鸭子类型(duck typing)[1],只要是跑起来像只马的,它就是一只马,也就能够参加赛马比赛。

class Horse {
public void race() {
System.out.println("Horse.race()");
}
} class Deer {
public void race() {
System.out.println("Deer.race()");
}
} class Cobra {
public void race() {
System.out.println("How do you turn this on?");
}
}

(如何用同一种方式调用他们的赛跑方法?)

说到了这里,如果我们将赛跑定义为对赛跑方法(对应上述代码中的 race())的调用的话,那么这个故事的关键,就在于能不能在马场中调用非马类型的赛跑方法。

为了解答这个问题,我们先来回顾一下 Java 里的方法调用。在 Java 中,方法调用会被编译为 invokestatic,invokespecial,invokevirtual 以及 invokeinterface 四种指令。这些指令与包含目标方法类名、方法名以及方法描述符的符号引用捆绑。在实际运行之前,Java 虚拟机将根据这个符号引用链接到具体的目标方法。

可以看到,在这四种调用指令中,Java 虚拟机明确要求方法调用需要提供目标方法的类名。在这种体系下,我们有两个解决方案。一是调用其中一种类型的赛跑方法,比如说马类的赛跑方法。对于非马的类型,则给它套一层马甲,当成马来赛跑。

另外一种解决方式,是通过反射机制,来查找并且调用各个类型中的赛跑方法,以此模拟真正的赛跑。

显然,比起直接调用,这两种方法都相当复杂,执行效率也可想而知。为了解决这个问题,Java 7 引入了一条新的指令 invokedynamic。该指令的调用机制抽象出调用点这一个概念,并允许应用程序将调用点链接至任意符合条件的方法上。

作为 invokedynamic 的准备工作,Java 7 引入了更加底层、更加灵活的方法抽象 :方法句柄(MethodHandle)。

方法句柄的概念

方法句柄是一个强类型的,能够被直接执行的引用 [2]。该引用可以指向常规的静态方法或者实例方法,也可以指向构造器或者字段。当指向字段时,方法句柄实则指向包含字段访问字节码的虚构方法,语义上等价于目标字段的 getter 或者 setter 方法。

这里需要注意的是,它并不会直接指向目标字段所在类中的 getter/setter,毕竟你无法保证已有的 getter/setter 方法就是在访问目标字段。

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

打个比方,如果兔子的“赛跑”方法和“睡觉”方法的参数类型以及返回类型一致,那么对于兔子递过来的一个方法句柄,我们并不知道会是哪一个方法。

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

当使用后者这种查找方式时,用户需要区分具体的调用类型,比如说对于用 invokestatic 调用的静态方法,我们需要使用 Lookup.findStatic 方法;对于用 invokevirutal 调用的实例方法,以及用 invokeinterface 调用的接口方法,我们需要使用 findVirtual 方法;对于用 invokespecial 调用的实例方法,我们则需要使用 findSpecial 方法。

调用方法句柄,和原本对应的调用指令是一致的。也就是说,对于原本用 invokevirtual 调用的方法句柄,它也会采用动态绑定;而对于原本用 invkespecial 调用的方法句柄,它会采用静态绑定。

方法句柄同样也有权限问题。但它与反射 API 不同,其权限检查是在句柄的创建阶段完成的。在实际调用过程中,Java 虚拟机并不会检查方法句柄的权限。如果该句柄被多次调用的话,那么与反射调用相比,它将省下重复权限检查的开销。

需要注意的是,方法句柄的访问权限不取决于方法句柄的创建位置,而是取决于 Lookup 对象的创建位置。

举个例子,对于一个私有字段,如果 Lookup 对象是在私有字段所在类中获取的,那么这个 Lookup 对象便拥有对该私有字段的访问权限,即使是在所在类的外边,也能够通过该 Lookup 对象创建该私有字段的 getter 或者 setter。

由于方法句柄没有运行时权限检查,因此,应用程序需要负责方法句柄的管理。一旦它发布了某些指向私有方法的方法句柄,那么这些私有方法便被暴露出去了。

方法句柄的操作

方法句柄的调用可分为两种,一是需要严格匹配参数类型的 invokeExact。它有多严格呢?假设一个方法句柄将接收一个 Object 类型的参数,如果你直接传入 String 作为实际参数,那么方法句柄的调用会在运行时抛出方法类型不匹配的异常。正确的调用方式是将该 String 显式转化为 Object 类型。

在普通 Java 方法调用中,我们只有在选择重载方法时,才会用到这种显式转化。这是因为经过显式转化后,参数的声明类型发生了改变,因此有可能匹配到不同的方法描述符,从而选取不同的目标方法。调用方法句柄也是利用同样的原理,并且涉及了一个签名多态性(signature polymorphism)的概念。(在这里我们暂且认为签名等同于方法描述符。)

方法句柄 API 有一个特殊的注解类 @PolymorphicSignature。在碰到被它注解的方法调用时,Java 编译器会根据所传入参数的声明类型来生成方法描述符,而不是采用目标方法所声明的描述符。

在刚才的例子中,当传入的参数是 String 时,对应的方法描述符包含 String 类;而当我们转化为 Object 时,对应的方法描述符则包含 Object 类。

如果你需要自动适配参数类型,那么你可以选取方法句柄的第二种调用方式 invoke。它同样是一个签名多态性的方法。invoke 会调用 MethodHandle.asType 方法,生成一个适配器方法句柄,对传入的参数进行适配,再调用原方法句柄。调用原方法句柄的返回值同样也会先进行适配,然后再返回给调用者。

方法句柄还支持增删改参数的操作,这些操作都是通过生成另一个方法句柄来实现的。这其中,改操作就是刚刚介绍的 MethodHandle.asType 方法。删操作指的是将传入的部分参数就地抛弃,再调用另一个方法句柄。它对应的 API 是 MethodHandles.dropArguments 方法。

增操作则非常有意思。它会往传入的参数中插入额外的参数,再调用另一个方法句柄,它对应的 API 是 MethodHandle.bindTo 方法。Java 8 中捕获类型的 Lambda 表达式便是用这种操作来实现的,下一篇我会详细进行解释。

增操作还可以用来实现方法的柯里化 [3]。举个例子,有一个指向 f(x, y) 的方法句柄,我们可以通过将 x 绑定为 4,生成另一个方法句柄 g(y) = f(4, y)。在执行过程中,每当调用 g(y) 的方法句柄,它会在参数列表最前面插入一个 4,再调用指向 f(x, y) 的方法句柄。

方法句柄的实现

下面我们来看看 HotSpot 虚拟机中方法句柄调用的具体实现。(由于篇幅原因,这里只讨论 DirectMethodHandle。)

前面提到,调用方法句柄所使用的 invokeExact 或者 invoke 方法具备签名多态性的特性。它们会根据具体的传入参数来生成方法描述符。那么,拥有这个描述符的方法实际存在吗?对 invokeExact 或者 invoke 的调用具体会进入哪个方法呢?

import java.lang.invoke.*;

public class Foo {
public static void bar(Object o) {
new Exception().printStackTrace();
} public static void main(String[] args) throws Throwable {
MethodHandles.Lookup l = MethodHandles.lookup();
MethodType t = MethodType.methodType(void.class, Object.class);
MethodHandle mh = l.findStatic(Foo.class, "bar", t);
mh.invokeExact(new Object());
}
}

结果:

$ java Foo
java.lang.Exception
at Foo.bar(Foo.java:5)
at Foo.main(Foo.java:12)

也就是说,invokeExact 的目标方法竟然就是方法句柄指向的方法。

先别高兴太早。我刚刚提到过,invokeExact 会对参数的类型进行校验,并在不匹配的情况下抛出异常。如果它直接调用了方法句柄所指向的方法,那么这部分参数类型校验的逻辑将无处安放。因此,唯一的可能便是 Java 虚拟机隐藏了部分栈信息。

当我们启用了 -XX:+ShowHiddenFrames 这个参数来打印被 Java 虚拟机隐藏了的栈信息时,你会发现 main 方法和目标方法中间隔着两个貌似是生成的方法。

$ java -XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames Foo
java.lang.Exception
at Foo.bar(Foo.java:5)
at java.base/java.lang.invoke.DirectMethodHandle$Holder. invokeStatic(DirectMethodHandle$Holder:1000010)
at java.base/java.lang.invoke.LambdaForm$MH000/766572210. invokeExact_MT000_LLL_V(LambdaForm$MH000:1000019)
at Foo.main(Foo.java:12)

总结与实践

今天我介绍了 invokedynamic 底层机制的基石:方法句柄。

方法句柄是一个强类型的、能够被直接执行的引用。它仅关心所指向方法的参数类型以及返回类型,而不关心方法所在的类以及方法名。方法句柄的权限检查发生在创建过程中,相较于反射调用节省了调用时反复权限检查的开销。

方法句柄可以通过 invokeExact 以及 invoke 来调用。其中,invokeExact 要求传入的参数和所指向方法的描述符严格匹配。方法句柄还支持增删改参数的操作,这些操作是通过生成另一个充当适配器的方法句柄来实现的。

方法句柄的调用和反射调用一样,都是间接调用,同样会面临无法内联的问题。

JVM总结-invokedynamic的更多相关文章

  1. JVM方法调用过程

    JVM方法调用过程 重载和重写 同一个类中,如果出现多个名称相同,并且参数类型相同的方法,将无法通过编译.因此,想要在同一个类中定义名字相同的方法,那么它们的参数类型必须不同.这种方法上的联系就是重载 ...

  2. 【JVM】JVM系列之类加载机制(四)

    一.前言 前面分析了class文件具体含义,接着需要将class文件加载到虚拟机中,这个过程是怎样的呢,下面,我们来仔细分析. 二.什么是类加载机制 把class文件加载到内存,并对数据进行校验.转换 ...

  3. 深入理解JVM内幕(转)

    转自:http://blog.csdn.net/zhoudaxia/article/details/26454421/ 每个Java开发者都知道Java字节码是执行在JRE((Java Runtime ...

  4. 漫谈JVM

    背景介绍 JVM已经是Java开发的必备技能了,JVM相当于Java的操作系统. JVM,java virtual machine, 即Java虚拟机,是运行java class文件的程序. Java ...

  5. 谈谈我印象中的JVM不足之处

    研究JVM也有一段时间了,其间也发现了它的很多不足之处,在此一一道来,由于本人对JVM的理解有限,如有错误的地方,还请大家指正:本文不介绍名词性术语和概念性知识,如有不了解的地方可Search Goo ...

  6. JVM知识点

    先发个链接到两位大牛的主页 http://rednaxelafx.iteye.com/               http://icyfenix.iteye.com/ 目录 1)概述 2)编译 3) ...

  7. 深入理解JVM内幕:从基本结构到Java 7新特性

    转自:http://www.importnew.com/1486.html 每个Java开发者都知道Java字节码是执行在JRE((Java Runtime Environment Java运行时环境 ...

  8. JVM学习笔记:虚拟机的类加载机制

    JVM类加载机制分两部分来总结: (1)类加载过程 (2)类加载器 一.JVM类加载过程 类的加载过程:加载 →连接(验证 → 准备 → 解析)→ 初始化. 类的生命周期:加载 →连接(验证 → 准备 ...

  9. [转]JVM内幕:Java虚拟机详解

    本文由 ImportNew - 挖坑的张师傅 翻译自 jamesdbloom.欢迎加入翻译小组.转载请见文末要求. 这篇文章解释了Java 虚拟机(JVM)的内部架构.下图显示了遵守Java SE 7 ...

随机推荐

  1. 3、eclipse集成svn

    手动安装 1.从官网下载适合自己开发工具的site-.zip文件,网址是:subclipse.tigris.org,2.从中解压出features与plugins文件夹,复制到eclipse的目录my ...

  2. WEKA从sqlite数据库文件导入数据

    1.编写代码的方式 只需要在java工程中导入weka.jar和sqlite-jdbc-3.8.7.jar两个jar包, weka.jar可以在weka的安装路径下找到, sqlite-jdbc-3. ...

  3. git命令的简单使用

    Gitbash初始化设置 Gitbash安装成功后要配置email和name,否则commit的时候会报错: 运行 git config --global user.email "你的ema ...

  4. modbus tcp 入门详解

    Modbus tcp 格式说明 通讯机制 附C#测试工具用于学习,测试   前言: 之前的博客介绍了如何用C#来读写modbus tcp服务器的数据,文章:http://www.cnblogs.com ...

  5. table 设置每列的颜色

    ISBN Title Price 3476896 My first HTML $53 5869207 My first CSS $49   <!DOCTYPE html> <html ...

  6. C++进阶--代码复用 继承vs组合

    //############################################################################ /* * 代码复用: 继承 vs 组合 * ...

  7. elasticsearch 口水篇(9)Facet

    FACET 1)Terms Facet { "query" : { "match_all" : { } }, "facets" : { &q ...

  8. Osip2和eXosip协议栈的简析

    Osip2是一个开放源代码的sip协议栈,是开源代码中不多使用C语言写的协议栈之一,它具有短小简洁的特点,专注于sip底层解析使得它的效率比较高. eXosip是Osip2的一个扩展协议集,它部分封装 ...

  9. python selenium 模拟登陆百度账号

    代码: from selenium import webdriver url = 'https://passport.baidu.com/v2/?login' username = 'your_use ...

  10. for循环计算li的个数

    今天有一段代码 在ie6下面显示 我检查了一下代码,发现每for循环一次,就会重新计算li的个数,会拖慢运行速度,所以改成以下代码,ie6就正常了