04_jvm_09_invokedynamic

第五十五章 invokedynamic指令与Lambda表达式的实现

1 简介

Java 7 引入了一条新的JVM指令invokedynamic。从指令的名字可以看出,这条指令的引入是为了增强Java语言的动态性的。那么,什么是动态语言呢?Java设计者认为动态语言需要具备在程序运行时检查数据类型的能力(Type Checking at Runtime)。因为Java程序都是由Java编译器翻译成.class文件的。在编译的过程中,Java编译器会检查Java程序的类型。由Java编译器做的类型检查被称为静态类型检查(Type Checking at Compile Time)。那么,这就回到了我们最初的问题:为什么Java语言需要增强动态性。

答案是:Java虚拟机是一个统一的运行平台,它不仅可以运行Java程序,它还可以运行从任何语言翻译成.class文件的程序。一旦被翻译成.class文件之后,这些程序可以与Java语言融合在一起,相互调用对方的函数,使用对方创建的对象,就像它们都是Java程序一样。所以,当Java虚拟机加载.class文件时,需要动态的检查数据类型。在整个动态特性的设计中,invokedynamic起着动态调用函数的作用和目的。

指令invokedynamic并不对应任何Java语言中的关键字、表达式或者语句。换句话说,Java开发者无法在Java语言的源代码中控制或者使用invokedynamic指令。是否使用invokedynamic指令是由Java编译器决定的。如果开发人员使用的是其他语言,那么,是否使用invokedynamic指令是由这门语言的编译器决定的。另一种常见的方法是使用Java虚拟机二进制代码操作库(Java Bytecode Manipulation Library)生成.class文件,有兴趣的读者可参考ASM

2 invokedynamic工作原理

在了解了invokedynamic指令的设计目的之后,我们再来介绍一下invokedynamic指令的工作原理。

与invokedynamic指令一起引入的是java.lang.invoke标准库包。为了表达一个函数调用,Java标准库中java.lang.invoke.MethodHandle用于表达一个函数,而java.lang.invoke.CallSite用于表达一个函数调用。

图一展示了invokedynamic指令背后的结构和意义。invokedynamic是两个词,第一个词invoke表示“调用”;Java虚拟机使用了Bootstrap Method这个概念触发/启动这个调用的过程。第二个词dynamic表示“动态”的调用对象,这个对象是由MethodHandle类和CallSite类共同表示的。

图一 invokedynamic原理图

图一 invokedynamic原理图。

invokedynamic指令有两个参数,第一个参数指向了一个动态创建的CallSite对象(用于表示一个函数调用),而第二个参数必须是0。这个动态创建的CallSite对象绑定了一个MethodHandle,用于表达函数调用的对象(即,待调用的函数)。这个函数可以是任意的函数,例如成员方法、构造函数、甚至是一个成员变量。当该函数被调用时,函数使用的参数将从操作数栈中弹出,就像我们上一节讲解的invokevirual、invokestatic和invokespecial指令那样。

在上述的过程中,由invokedynamic指令的参数找到动态创建的CallSite对象是由一个特殊的方法完成的。这个方法被称为Bootstrap方法(Bootstrap Method)。实际上,invokedynamic指令的参数是指向了常量池中的CONSTANT_InvokeDynamic_Info的元素,而CONSTANT_InvokeDynamic_Info包含了指向Bootstrap Method的信息。当Java虚拟机(Java解释器)运行invokedynamic指令时,Java虚拟机(Java解释器)会找到这个Bootstrap方法,并调用这个Bootstrap方法动态创建一个CallSite对象。然后,Java虚拟机再调用CallSite对象指向的函数。

所以,我们总结一下invokedynamic指令的执行过程,如下图所示。当Java虚拟机运行invokedynamic指令时,首先根据指令的第一个参数找到一个Bootstrap方法。然后,调用这个Bootstrap方法获得一个动态的CallSite对象。因为,这个对象表示着一个函数调用,所以,可以从这个CallSite对象中获得待调用的函数(由MethodHandle表示)。最后,Java虚拟机调用这个函数。函数的参数从操作数栈的栈顶获取。

图二 invokedynamic执行流程

图二 invokedynamic执行流程。

3 Lambda表达式的实现

虽然invokedynamic指令的设计初衷是为了支持非Java语言运行在Java虚拟机上,并能够与Java程序融合在一起。然而,由于invokedynamic指令的诸多优势(尤其是性能上的优势),使得Java设计者们开始考虑将invokedynamic指令用于Java程序。Lambda表达式的实现就是一个典型的例子。

我们将用下面的一个例子来解释Java设计者们为什么使用invokedynamic来实现Lambda表达式。他们选择invokedynamic的原因是invokedynamic运行更快。在Lambda表达式的实现上,Java设计者考虑了多种备选方法,其中主要的两个方法是 (1) 传统的方法:Java编译器为Lambda表达式生成一个匿名类;(2) 使用invokedynamic指令。

例如,假设有如下代码,在main()函数中定义了一个Predicate(判断式)的变量isString,用于判断一个对象是否为String类型。该语句的右侧是一个Lambda表达式,使用关键字instanceof判断参数x是否为String类型。

import java.util.function.Predicate;

public class PredicateExample {
    public static void main(String[] args) {
        Predicate isString = x -> x instanceof String;
        isString.test("This is a String");
    }
}

如果采用传统方法,Java编译器会将其转换为如下的等效代码。因为Predicate是一个Functional Interface,Java编译器会生成一个内部匿名类AnonymousLambdaClass (为了展示出等效代码,此例必须给一个类名,在实际运用中,Java编译器会根据规则生成一个更复杂的类名)并实现Predicate接口。Lambda表达式被移至test()成员方法中。而在main()函数中,程序则创建一个AnonymousLambdaClass的对象。

import java.util.function.Predicate;

public class PredicateExample {
    public static void main(String[] args) {
        Predicate isString = new AnonymousLambdaClass();
        isString.test("This is a String");
    }

    private static class AnonymousLambdaClass<T> implements Predicate<T> {
        @Override
        public boolean test(T x) {
            return x instanceof String;
        }
    }
}

这种传统方法能够很好的实现Lambda表达式,然而,它在性能方面则存在一些瑕疵。例如,这种传统方法会生成一个额外的内部匿名类。根据目前Java编译器的工作原理,它会为每个类生成一个.class文件。因此,当Java虚拟机第一次创建AnonymousLambdaClass类的对象时,Java虚拟机需要从文件系统中加载对应的.class文件。这个加载过程涉及到在CLASSPATH中搜索一个文件。这个搜索过程是非常昂贵的(至少包括多个I/O操作)。

为了避免这个搜索过程,Java的设计者采用了invokedynamic指令来实现Lambda表达式。在PredicateExample类对应的.class文件中,加入了一个Bootstrap方法。这个Bootstrap方法动态的创建一个CallSite对象。然后,Java虚拟机会调用这个CallSite对象,动态创建一个匿名类AnonymousLambdaClass的对象。然后,接下来,Java程序就可以调用这个对象的test()方法检测输入参数是否为String类型的对象了。因此,简而言之,Java编译器会使用invokedynamic指令动态的创建一个匿名类对象,而不是像传统方法那样使用new操作符创建一个匿名类对象,从而避免了在CLASSPATH中搜寻.class文件,加快了Java虚拟机的执行速度。

如果我们把最初的例子编译并反汇编查看Java指令的话,我们能够发现main()函数的第一条指令是invokedynamic,用于创建对象;而不是使用的new指令创建对象。

>javac PredicateExample.java
>javap -c PredicateExample
Compiled from "PredicateExample.java"
public class PredicateExample {
  public PredicateExample();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: invokedynamic #2,  0              // InvokeDynamic #0:test:()Ljava/util/function/Predicate;
       5: astore_1
       6: aload_1
       7: ldc           #3                  // String This is a String
       9: invokeinterface #4,  2            // InterfaceMethod java/util/function/Predicate.test:(Ljava/lang/Object;)Z
      14: pop
      15: return
}

4 总结

本章介绍了指令invokedynamic的工作原理和Lambda表达式的实现。Java语言的设计者为了支持非Java语言的运行,提出了一条新的指令invokedynamic。这条指令能分离函数的调用和函数的选择过程,为支持和优化非Java语言的运行提供了极大的便利性和灵活性。正是这个优点,Java设计者将其用于Lambda表达式的实现中。Lambda表达式的实现方法有很多,但是目前为了达到最高的运行效率,Java设计者采用了invokedynamic指令。在未来的实现版本中,Java设计者或许能找到更好更快的实现方法。

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.