1. 概述

InvokeDynamic(常简称为 Indy)是 JSR 292 的核心成果,旨在增强 JVM 对动态类型语言的支持。自 Java 7 引入 invokedynamic 字节码指令以来,它已被广泛应用于 JRuby 等动态语言,甚至在 Java 这类静态语言中也大显身手。

本文将带你揭开 invokedynamic 的神秘面纱,看看它如何帮助语言和库的设计者实现各种形式的动态行为。

2. 初识 InvokeDynamic

我们从一个简单的 Stream API 链式调用开始:

public class Main { 

    public static void main(String[] args) {
        long lengthyColors = List.of("Red", "Green", "Blue")
          .stream().filter(c -> c.length() > 3).count();
    }
}

直觉上,你可能认为 Java 会为 c -> c.length() > 3 创建一个实现 Predicate 接口的匿名内部类,然后把这个实例传给 filter 方法。

但,这是错的。

2.1. 字节码分析

让我们用 javap 查看生成的字节码来验证:

javap -c -p Main
// 简化了类名便于阅读,例如 Stream 实际为 java/util/stream/Stream
0: ldc               #7             // String Red
2: ldc               #9             // String Green
4: ldc               #11            // String Blue
6: invokestatic      #13            // InterfaceMethod List.of:(LObject;LObject;)LList;
9: invokeinterface   #19,  1        // InterfaceMethod List.stream:()LStream;
14: invokedynamic    #23,  0        // InvokeDynamic #0:test:()LPredicate;
19: invokeinterface  #27,  2        // InterfaceMethod Stream.filter:(LPredicate;)LStream;
24: invokeinterface  #33,  1        // InterfaceMethod Stream.count:()J
29: lstore_1
30: return

结果出人意料:根本没有创建匿名内部类,也没有传递任何类实例给 filter 方法。

关键就在第 14 行的 invokedynamic 指令,它才是创建 Predicate 实例的幕后推手。

2.2. Lambda 生成的特殊方法

此外,编译器还生成了一个名为 lambda$main$0 的静态方法:

private static boolean lambda$main$0(java.lang.String);
    Code:
       0: aload_0
       1: invokevirtual #37                 // Method java/lang/String.length:()I
       4: iconst_3
       5: if_icmple     12
       8: iconst_1
       9: goto          13
      12: iconst_0
      13: ireturn

这个方法接收一个 String 参数,执行以下逻辑:

  • ✅ 调用 length() 获取字符串长度
  • ✅ 将长度与常量 3 比较
  • ✅ 若长度 <= 3,返回 false

这不正是我们传给 filter 的 lambda 表达式 c -> c.length() > 3 的逻辑吗?

结论:Java 没有使用匿名内部类,而是生成了一个静态方法,并通过 invokedynamic 来调用它。

接下来,我们深入探究这一机制的内部原理。但在此之前,先搞清楚 invokedynamic 要解决的核心问题。

2.3. 问题背景

在 Java 7 之前,JVM 只有四种方法调用指令:

  • invokevirtual:调用实例方法
  • invokestatic:调用静态方法
  • invokeinterface:调用接口方法
  • invokespecial:调用私有或构造方法

这些指令的共同点是:它们的调用流程是预定义且固化的,开发者无法插入自定义逻辑。

面对这一限制,通常有两种“补丁”方案:

  1. 编译时方案:如 Scala、Kotlin 使用的代码生成。运行时效率高,但可能导致启动变慢(字节码膨胀),且不够灵活。
  2. 运行时方案:如 JRuby 使用的反射。足够灵活,但性能开销大,是典型的“慢路径”。

invokedynamic 的出现,就是为了提供一种既灵活又高效的新方案。

3. 工作原理揭秘

invokedynamic 的核心在于:它允许我们自定义方法调用的整个引导过程

当 JVM 第一次遇到某个 invokedynamic 指令时,它会调用一个特殊的“启动方法”(Bootstrap Method)来初始化调用过程:

Untitled Diagram

  • Bootstrap Method 是一段我们编写的普通 Java 代码,可以包含任意逻辑。
  • ✅ 它执行完成后,必须返回一个 CallSite 实例。
  • CallSite 包含两个关键信息:
    • 一个指向实际执行逻辑的 MethodHandle
    • 一个表示该 CallSite 有效性的条件。

之后,JVM 再次执行到该 invokedynamic 指令时,就会跳过“启动”这个慢步骤,直接通过 CallSite 调用底层逻辑。只有当 CallSite 的有效性条件改变时,才会重新触发启动过程。

与反射相比,MethodHandle 是 JVM 可见的,因此 JVM 可以对其进行深度优化,性能远超反射。

3.1. 启动方法表 (Bootstrap Method Table)

再看一眼字节码中的 invokedynamic 指令:

14: invokedynamic #23,  0  // InvokeDynamic #0:test:()Ljava/util/function/Predicate;
  • #0 表示它要调用启动方法表中的第一个启动方法。
  • test:()LPredicate 表示要调用的方法名为 test,签名为 () -> Predicate

要查看启动方法表,需使用 javap -v

javap -c -p -v Main
// ...
BootstrapMethods:
  0: #55 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:
    (Ljava/lang/invoke/MethodHandles$Lookup;
     Ljava/lang/String;
     Ljava/lang/invoke/MethodType;
     Ljava/lang/invoke/MethodType;
     Ljava/lang/invoke/MethodHandle;
     Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #62 (Ljava/lang/Object;)Z
      #64 REF_invokeStatic Main.lambda$main$0:(Ljava/lang/String;)Z
      #67 (Ljava/lang/String;)Z

所有 Lambda 的启动方法都是 LambdaMetafactory.metafactory

该方法接收的参数包括:

  • MethodHandles$Lookup:查找上下文。
  • String:调用点的方法名(这里是 test)。
  • MethodType:调用点的动态方法签名(() -> Predicate)。

此外,它还接收三个额外参数:

  • (Ljava/lang/Object;)Z:目标接口方法的擦除后签名。
  • REF_invokeStatic ...:指向实际 lambda 逻辑的 MethodHandle
  • (Ljava/lang/String;)Z:目标接口方法的非擦除签名。

简单说:JVM 把所有信息打包给 metafactory,它负责创建出能正确桥接的 Predicate 实例。

3.2. 不同类型的 CallSite

首次执行时,JVM 调用 metafactory。它内部会使用 InnerClassLambdaMetafactory 在运行时动态生成一个实现 Predicate 的内部类。

然后,启动方法将这个生成的类封装在一个 ConstantCallSite 中。这种 CallSite 一旦创建就永不改变。

  • ✅ **ConstantCallSite**:最高效,适用于“一劳永逸”的场景(如 Lambda)。
  • ⚠️ **MutableCallSite**:可变,允许在运行时切换目标 MethodHandle
  • ⚠️ **VolatileCallSite**:可变且提供 volatile 语义,适用于需要线程安全的动态场景。

3.3. 优势总结

相比在编译时生成匿名内部类,invokedynamic 方案有显著优势:

  • 延迟生成:内部类直到首次使用 Lambda 时才生成,避免了无谓的类加载开销。
  • 字节码更小:调用逻辑被移到启动方法,.class 文件更紧凑,有助于提升启动速度。
  • 二进制兼容性:如果未来 JVM 优化了 LambdaMetafactory,现有字节码无需重新编译即可受益。
  • 更易维护:用 Java 代码编写启动逻辑,比直接生成复杂字节码更简单、不易出错。

4. 更多应用场景

invokedynamic 不仅用于 Lambda,Java 本身的许多新特性也依赖它。

4.1. Java 14:Records

Records 提供了声明纯数据类的简洁语法:

public record Color(String name, int code) {}

编译器会自动生成 toStringequalshashCode 等方法。而这些方法的实现就用到了 invokedynamic

例如 equals 方法的字节码:

public final boolean equals(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: invokedynamic #27,  0  // InvokeDynamic #0:equals:(LColor;Ljava/lang/Object;)Z
       7:ireturn
  • 传统方案:编译时根据字段数生成对应逻辑,字段越多,字节码越长。
  • InvokeDynamic 方案:无论字段多少,字节码长度恒定。启动方法 ObjectMethods.bootstrap 在运行时链接正确的实现。

启动方法表信息:

BootstrapMethods:
  0: #42 REF_invokeStatic java/lang/runtime/ObjectMethods.bootstrap:
    ...
    Method arguments:
      #8 Color
      #49 name;code
      #51 REF_getField Color.name:Ljava/lang/String;
      #52 REF_getField Color.code:I

4.2. Java 9:字符串拼接

在 Java 9 之前,复杂的字符串拼接(如 "a" + obj + "b")会生成 StringBuilder 代码。从 Java 9 开始(JEP 280),改用 invokedynamic

例如:

"random-" + ThreadLocalRandom.current().nextInt();

其字节码为:

0: invokestatic  #7          // Method ThreadLocalRandom.current:()LThreadLocalRandom;
3: invokevirtual #13         // Method ThreadLocalRandom.nextInt:()I
6: invokedynamic #17,  0     // InvokeDynamic #0:makeConcatWithConstants:(I)LString;

启动方法是 StringConcatFactory.makeConcatWithConstants

BootstrapMethods:
  0: #30 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:
    ...
    Method arguments:
      #36 random-\u0001

JVM 可以根据字符串复杂度选择最优策略(如直接 String.concatStringBuilderStringLatin1.inflate),这一切对开发者透明。

5. 总结

invokedynamic 是 JVM 进化史上的一个里程碑。

  • 它解决了静态指令集在动态场景下的性能与灵活性困境。
  • 通过启动方法和 CallSite 机制,实现了“一次引导,长期高效”的调用模式。
  • Lambda、Records、字符串拼接等现代 Java 特性都受益于此。

理解 invokedynamic 不仅能帮你避开一些性能“坑”,更能让你深刻体会到 JVM 的设计之美。下次看到 invokedynamic 字节码,你就知道,这背后是一套精巧的动态链接系统在默默工作。


原始标题:An Introduction to Invoke Dynamic in the JVM | Baeldung