32

JVM之动态方法调用:invokedynamic - Java译站

 4 years ago
source link: http://it.deepinmind.com/jvm/2019/07/19/jvm-method-invocation-invokedynamic.html?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

JVM之动态方法调用:invokedynamic

Published: 19 Jul 2019 Category: JVM

在本文的前面的姊妹篇中,介绍了Java方法调用的5种操作码中的4种。它们是Java 8和Java 9中方法调用的标准字节码形式。

于是第五个操作码invokedynamic便进入了我们的视线。简单来说,Java 7中在语言层面上对invokedynamic是没有直接支持的。事实上,当Java 7的运行时首次引入invokedynamic指令时,javac编译器是不会生成这个字节码的。

而到了Java 8中,invokedynamic则成为了实现高级平台特性的一个首要机制。使用这个操作码的最明确、简单的例子便是lambda表达式。读这篇文章前需要熟悉一下JVM的方法调用,也可以先读一下本文的前一姊妹篇

lambda的实质是对象引用

在开始深入介绍invokedynamic是如何赋能lambda表达式前,我们先简单介绍下什么是lambda表达式。Java只有两种值的类型:基础类型(比如char,int等等)和对象引用。lambda显然并不是基础类型,那它只能是对象引用了。看下这个lambda表达式:

public class LambdaExample {
    private static final String HELLO = "Hello";

    public static void main(String[] args) throws Exception {
        Runnable r = () -> System.out.println(HELLO);
        Thread t = new Thread(r);
        t.start();
        t.join();
    } 
}

5行的lambda表达式被赋值给了Runnable类型的变量。这说明lambda变成了一个兼容Runnable类型的对象的引用。本质上来说,这个对象的类型应该是Object的某个子类,它额外定义了一个方法(并且没有别的字段)。这个额外的方法就是Runnable所期望的run()方法。

Java 8以前,这样的对象只能通过一个具体实现了Runnable接口的匿名类来表示。事实上,Java 8的lambda表达式最初的实现原型也是内部类。

但从JVM的未来长期的roadmap来看,是希望能够支持更复杂的lambda表达式的。如果只能显式地通过内部类来实现lambda表达式,未来版本的实现方式就会比较受限。而这并不是大家想要的,因此Java 8和Java 9采用了一种更复杂的技术,而不是硬编码为内部类。上面那个例子对应的字节码如下:

public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: invokedynamic #2,  0 // InvokeDynamic
                               // #0:run:()Ljava/lang/Runnable;

       5: astore_1
       6: new           #3       // class java/lang/Thread
       9: dup
       10: aload_1
       11: invokespecial #4      // Method java/lang/Thread."<init>":
                            // (Ljava/lang/Runnable;)V
       14: astore_2
       15: aload_2
       16: invokevirtual #5      // Method java/lang/Thread.start:()V
       19: aload_2
       20: invokevirtual #6      // Method java/lang/Thread.join:()V
       23: return

标记0处的字节码说明正使用invokedynamic来调用某个方法,并将返回值存储到栈上。接下来的字节码则对应着Java方法的剩余部分,这个比较容易理解。

invokedynamic是如何工作的

下面我将要介绍下invokedynamic的内部细节以及这个操作码是如何工作的。当类加载器加载了一个使用了invokedynamic指令的类时,要调用的目标方法是没法提前预知的。这个设计和JVM中其它方法调用的字节码都不一样。

比如说,在前文中提到的invokestatic和invokespecial,具体的实现方法(又叫调用目标)在编译时就已经确定了。而对于 invokevirtual和invokeinterface来说,调用目标在运行时才能确定。然而,选择的目标也是受限于Java语言规范的规则和类型系统的约束的。因此,至少有部分调用信息在编译期是能确定的。

相反,在具体调用哪个方法方面,invokedynamic要更灵活。在使用了invokedynamic的类的常量池中,会有一个特殊的常量,invokedynamic操作码正是通过它来实现这种灵活性的。这个常量包含了动态方法调用所需的额外信息,它又被称为引导方法(Bootstrap Method,BSM)。这是invokedynamic实现的关键,每个invokedynamic的调用点(call site)都对应着一个BSM的常量池项。为了将BSM关联到某个特定的invokedynamic调用点上,Java 7的类文件格式中新增了一个InvokeDynamic类型的新常量池项。

invokedynamic指令的调用点在类加载时是“未链接的(unlaced)”。调用BSM后才能确定具体要调用的方法,返回的这个CallSite对象会被关联到调用点上。

最简单的调用点是ConstantCallSite,一旦完成查找便无需重复执行。后续的调用都会直接唤起这个调用点的目标对象,不再需要任何额外的工作。就是说调用点是稳定的,也即是对诸如JIT编译器的JVM子系统是友好的。

这套机制要想高效运行,JDK就必须要有合适的类型来表示调用点、BSM,以及其它的实现部分。Java最早核心的反射类型是包括方法及类型的。然而,这套API已经是非常古老了,有许多原因表明它并不是理想的选择。

举个例子,反射诞生之时还没有集合和泛型。因此对应的方法签名在反射API中只能通过Class[]来表示。这显得很笨重并且很容易出问题,并且Java数组的冗长语法也让它举步维艰。更不用说还要手工处理基础类型的装箱及拆箱操作,以及可能会出现的void返回值。

方法句柄(Method Handle)

为了不强制开发人员去处理这些问题,Java 7引入了一套新的API,叫方法句柄(Method Handle),用来提供必要的抽象。这套API的核心都在java.lang.invoke包中,尤其是MethodHandle这个类。这个类型的实例是可执行的,可以用它来调用某个方法。从参数和返回值可以看出,它们是动态类型的,这样就尽可能地保障了类型安全,可以动态去进行使用。这套API是invokedynamic的基础,但也可以单独使用,你可以把它看作是更现代、更安全的反射API。

方法句柄需要查找上下文才能获取到。常用的获取上下文的方式是调到静态方法MethodHandles.lookup()。它会返回一个基于当前执行方法的查询上下文。通过调用一系列的find*()(比如说,findVirtual()或findConstructor())方法,可以从这个上下文中获取到方法句柄。

方法句柄和反射的重要区别是,查询上下文只会返回查询对象创建时所在的域能访问的方法。这个是无法绕过的,也没有类似于反射里setAccessible()这样的后门。也就是说方法句柄始终是可以安全使用的,哪怕是和security manager一起使用。

但也还要注意,因为访问控制移到了方法查询阶段。就是说查询上下文可以返回查询时可见的私有方法,但不能保证这些方法句柄在调用时仍然可见。

为了解决方法签名如何表示的问题,MethodHandles还引入了MethodType类,这是一个简单的不可变类型,它有许多非常有用的特性。它可以:

  • 用来表示方法的类型签名
  • 包括返回值类型以及参数类型
  • 不包含接收者类型和方法名
  • 是设计来解决反射中的Class[]问题的

不光如此,它的实例还是不可变的。

有了这套API,方法签名便可以通过MethodType的实例来表示了,也不再需要为了每一个可能的签名去创建一个新的类型了。一个很简单的工厂方法便能够创建出新的实例:

// toString()
MethodType mtToString =
    MethodType.methodType(String.class);
// A setter method
MethodType mtSetter =
    MethodType.methodType(void.class, Object.class);
// compare() from Comparator<String>
MethodType mtStringComparator =
    MethodType.methodType(int.class, String.class, String.class);

一旦创建好了签名对象,便可以用它来查找方法句柄(还得加上方法名),正如下面的例子那样,查找toString()方法的句柄。

public MethodHandle getToStringHandle() {
    MethodHandle mh = null;
    MethodType mt = MethodType.methodType(String.class);
    MethodHandles.Lookup lk = MethodHandles.lookup();

    try {
        mh = lk.findVirtual(getClass(), "toString", mt);
         
    } catch (NoSuchMethodException | IllegalAccessException mhx) {
        throw new AssertionError().initCause(mhx);
    }

    return mh; 
}

可以类似调用反射API那样去invoke这个句柄。如果是实例方法则必须传入接收者对象,调用方代码也必须要处理可能出现的各种异常。

MethodHandle mh = getToStringMH();

try {
    mh.invoke(this, null);
} catch (Throwable e) {
    e.printStackTrace();
}

那么现在BSM的概念应该就更明确了:当程序首次执行到invokedynamic的调用点时,会去调用相关联的BSM。BSM会返回一个调用点对象,它包含了一个方法句柄,会指向最终绑定到这个调用点上的方法。在静态类型系统中,这套机制如果要正确运行,BSM就必须返回一个签名正确的方法句柄。

再回到之前Lambda表达式的那个例子中,invokedynamic可以看作是调用了平台的某个lambda表达式的工厂方法。实际的lambda表达式则被转换成了该表达式所在类的一个静态私有方法。

private static void lambda$main$0();
    Code:

       0: getstatic     #7  // Field
                            //  java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #9  // String Hello
       5: invokevirtual #10 // Method
                            //  java/io/PrintStream.println:
                            //  (Ljava/lang/String;)V
       8: return

lambda工厂方法会返回一个实现了Runnable类型的的实例对象,lambda执行时该类型的run()方法会回调到这个私有方法上。

通过javap -v可以看到这个常量池项:

#2 = InvokeDynamic #0:#40 //
#0:run:()Ljava/lang/Runnable;

class文件中的BSM里也可以找到这个被调用的工厂方法:

BootstrapMethods:
  0: #37 REF_invokeStatic
java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodH
andles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/i
nvoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodT
ype;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #38 ()V
      #39 REF_invokeStatic optjava/LambdaExample.lambda$main$0:()V
      #38 ()V

这里包含一个叫metafactory()的静态工厂方法,它是在java.lang.invoke下的LambdaMetafactory类中的。lambda的静态方法生成之后,这个BSM方法便会在运行时生成连接的字节码。metafactory的入参包括查询对象、用来确保静态类型安全的方法类型(method types),以及指向lambda表达式的静态私有方法的方法句柄。

public static CallSite metafactory(
        MethodHandles.Lookup caller,
        String invokedName,
        MethodType invokedType,
        MethodType samMethodType,
        MethodHandle implMethod,
        MethodType instantiatedMethodType)
            throws LambdaConversionException {
    AbstractValidatingLambdaMetafactory mf;
    mf = new InnerClassLambdaMetafactory(
            caller, invokedType,
            invokedName, samMethodType,
            implMethod, instantiatedMethodType,
            false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
    mf.validateMetafactoryArgs();
    return mf.buildCallSite();
}

目前所使用的这个metafactory方法,仍会为每个lambda表达式生成一个内部类,但这些类是动态生成的,也不会回写到磁盘上。这个实现机制在未来的Java版本中可能会发生变化,这样原有的lambda表达式也都能受益于后续新的实现。

在Java 8和Java 9中,InnerClassLambdaMetafactory的实现使用了一个轻微修改过的ASM字节码库,它发布在jdk.internal.org.objectweb.asm包下。

它能动态地生成lambda表达式的实现类,同时还保证了未来的可扩展性和对JIT的友好性。

它的实现方式也是最简单的——调用点一旦返回后就不会再变化了。返回的调用点类型便是之前提到过的ConstantCallSite。invokedynamic还能支持更复杂的场景,比如调用点可以是可变的甚至是实现volatile变量类似的语义。当然这些情况也更复杂、更难以处理,但它们为平台的动态扩展提供了最大的可能性。

前面lambda表达式的例子揭示了invokedynamic操作码是如何在静态类型基础上进行关键性扩展的,它使得灵活的运行时分发成为了可能。

可能大部分开发人员对invokedynamic都不是很熟悉,但它的加入确实让Java生态系统得到了明显的提升。如果没有invokedynamic以及它背后所代表的方法执行过程的重塑,未来Java版本中很多VM的优化技术都无法实现。

英文原文链接

« JVM中方法调用的实现机制 Java 10的类型推导 »


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK