这可不是 Graal VM …


你听说过这些东西吗:

  • MethodHandle
  • invokedynamic
  • Continuation
  • 值类型

达芬奇的直升机

上面的东西大多都是在 Java 7 出现的,也是 mlvm 项目的主要产出,意图打造一个可以允许多种语言的虚拟机。这个项目又被称为 the Da Vinci Machine Project。

这张图就是出自达芬奇,已经快有 600 年的历史,而它描述的自然是一个直升机的设想。

mlvm 项目的历史也超过十年了。在 2009 年,mlvm 项目展现了雄伟的野心:

动态调用

动态调用也就是 Dynamic Invocation,已经在 Java 7 中很好地实现了 —— invokedynamic 指令。

这条指令,不同于已存在的四个指令,是允许动态调用的,同时也包含了其他的许多好处,比如消除了 int 这样的原始类型装箱拆箱的消耗。

它的动态调用允许 Java 代码更改某处调用点(调用点,当然就是 CallSite)实际调用的方法,就像写入一个字段一样轻松简单。而这里“实际调用的方法”,就是下面的轻量方法对象。

可是动态调用呢?

在本文写作时(Java 19 已发布),Java 本身不带有任何涉及动态调用的代码。虽说如此,Java 中还是有不少地方使用到了 invokedynamic

Lambda 表达式中,invokedynamic 指令用于生成接口的实现类。

从 Java 9 开始,invokedynamic 用于实现字符串拼接。

从 Java 16 开始,Record 类中 invokedynamic 用于生成 toString(该实现其实很不好)、equalshashCode 这样的方法。

从 Java 17 开始,switch 表达式也使用 invokedynamic 生成 switch table。

除了 Lambda 表达式的实现之外,其他几个调用的 MH 都是使用 MethodHandles 中的方法进行组合而实现。

以上内容都不是动态调用,反而倒更像是某种意义上 Java 的宏了。

轻量方法对象

Lightweight method objects,在 Java 7 中最终作为 MethodHandle 落地;更准确地描述可能是“一段代码”。

提到 MethodHandle 或者 MH,大多数人和互联网上的文章都会拿来和反射 API 进行比较,这里自然也拿来比较一下。

对于方法调用,反射 API 是有设计缺陷的:

  • getMethodgetField 这样的操作,并不允许提供方法返回值或者字段类型作为参数进行查询 —— 分明 JVM 中存在这样的机制,更别说 JVM 允许名称和参数列表项目而返回值不同的方法多个存在了;
  • 什么是 getMethod,什么又是 getDeclaredMethod 呢?在语言层面和 JVM 层面,只有“可访问”和“不可访问”的区别,反射 API(很长一段时间)没有区别可访问的能力;
  • 更别说 Method#invoke 的时候的装箱拆箱、可变长参数的数组创建、调用时权限检查等性能问题了。

Java 7 出现的 MH 解决了上面的问题,尽管它相比反射 API 缺少了很多功能。与反射 API 相比,MH 更符合一个动态调用 API 的定位:

  • 字节码怎么写,Lookup#findXXX 就怎么写;
  • 语义和字节码中这里能访问的东西,MH 就能访问;语义不能的,MH 就不能;
  • 字节码里怎么调用,MH 就怎么调用,不会多装箱,或者把抛出的一场包装一层 InvocationTargetException
  • 直接调用什么性能,MH 调用就是什么性能(一定条件下)。

当然,MH 缺少了调用的方法名称、声明的类之类的信息,只保存了调用方法的类型,所以称之为轻量方法对象。

LambdaForm - 另一个 JIT?

MH 不只是某种反射的替代品。在 JEP 160 中 JDK 引入了 LambdaForm,用于表达 MethodHandle。

LambdaForm 是一系列方法调用、若干个参数和临时变量的“一段代码”。为了简化表达,这些参数和变量只有五种类型,也就是引用类型和四个基础类型(intlongfloatdouble)和 void

以 LambdaForm 的文档为例:

(a0:J)=>{ a0 }
    == identity(long)
(a0:I)=>{ t1:V = System.out#println(a0); void }
    == System.out#println(int)
(a0:L)=>{ t1:V = System.out#println(a0); a0 }
    == identity, with printing side-effect
(a0:L, a1:L)=>{ t2:L = BoundMethodHandle#argument(a0);
                t3:L = Class#cast(t2,a1); t3 }
    == invoker for identity method handle which performs cast

LambdaForm 设计为可以轻易地翻译到字节码和 JIT IR,可以看作是字节码的抽象表达形式。

轻量字节码加载

Lightweight bytecode loading,可能你从来没有见过?

Java 15 包括了 JEP 371: Hidden Classes,或者说是隐藏类;而在 Java 15 之前,这样的功能通过 Unsafe#defineAnonymousClass 实现。

这个功能用于生成一些轻量的、用于调用其他方法的没有名称的类。

如果是调用方法的话,是不是和 MH 非常类似?它与 MH 最大的区别是,它是类,所以可以有字段,被用于 Lambda 表达式中的变量捕获。

接口注入

Interface injection,接口注入,动态语言中通常有这样的功能,可惜 JVM 尚且没有。

这个设计的原型描述1是相对保守的,注入接口后不会影响已有类的解析和调用,但是 instanceof 和类型转换会反馈注入的接口。

运行时接口注入虽然没有被 JDK 加入,但是 DCEVM 项目(现在是 JetbrainsRuntime)可以实现这一功能。Mixin 这样的项目也可以在类加载的时候注入接口。


什么是 Continuation?

Continuations

Continuation 就是程序后面的部分,或者说剩下的部分。

System.out.println("ザ・ワールド 時よ止まれ!");
Thread.sleep(5000);
System.out.println("ザ・ワールド 時は動き出す");

在执行到 Thread#sleep 时,自然最后一个 System.out.println 是当前的 Continuation。而将 Continuation 明确表示出来(比如一个 Lambda 表达式),并把它传递(passing)到其他地方的代码,就叫 continuation-passing style。

System.out.println("ザ・ワールド 時よ止まれ!");
Runnable continuation = () -> System.out.println("ザ・ワールド 時は動き出す");
CompletableFuture.delayedExecutor(5000, TimeUnit.MILLISECONDS).execute(continuation);

而不将 Continuation 明确表现出来,而是让 JVM 管理,就是这个项目的目标。

尽管 mlvm 项目的 continuation 并没有最终实现,但是另一个项目 loom 最终在 Java 19 实现了 JVM 上的 Continuation 和虚拟线程

尾递归

Tail calls/tail recursion,也就是尾调用、尾递归,在函数式编程中更为常见。对于尾递归,我们可以借用 Continuation 的概念,定义为“方法最后的调用的 Continuation 是其自身”。

mlvm 项目对于尾递归的实现是添加一系列新指令,比如 tailcallinvokestatic 这样。当然,这样的实现最终没有落地。

loom 项目似乎也会提供对于尾调用的支持,虽然目前还没有。

元组和值类型

元组(Tuple),可以简单的理解为几个东西的组合,好比一个坐标 Vector3i 就是 (x: int, y: int, z: int) 这样的元组。

值类型,是相对于引用类型而言的概念,这样的类型在 JVM 中直接保存在栈上,如 intlong 一样。

在 mlvm 项目中,值类型会被实现为类似 {LBlockPos;III} 这样的类型签名,通过 vaccess getfield 这样的指令读取字段,并且值类型是可变的。在现在看来,这样的设计自然是复杂且不好的。

值类型自然在 mlvm 项目没有落地。随后的 valhalla 项目接过了值类型的担子,并且同时准备提供泛型特化的特性。

后记

mlvm 项目未尝不是某种伊卡洛斯的坠落,它的目标在十年后仍在进行。

虽然 Graal 项目本身的目标是用 Java 编写 compiler,却实现了 mlvm 设想的 Multi Language VM。

写这篇文章的想法来自于我的一个项目 Arcturus,其实现了 JEP 303 和 Java 上的尾递归消除。