常见问题解答

以下是关于在 GraalVM 上运行 JavaScript 的最常见问题和答案。

兼容性 #

GraalJS 是否与 JavaScript 语言兼容? #

GraalJS 与 ECMAScript 2024 规范兼容,并随着 2025 草案规范的制定而不断发展。GraalJS 的兼容性由外部来源验证,例如 Kangax ECMAScript 兼容性表

GraalJS 在一组测试引擎上进行了测试,例如 ECMAScript 的官方测试套件 test262,以及 V8 和 Nashorn 发布的测试、Node.js 单元测试和 GraalJS 自己的单元测试。

有关描述 GraalVM 支持的 JavaScript API 的参考文档,请参阅 GRAAL.JS-API

我的应用程序以前在 Nashorn 上运行,为什么它在 GraalJS 上无法运行? #

原因

  • GraalJS 试图与 ECMAScript 规范以及竞争引擎(包括 Nashorn)兼容。在某些情况下,这是一个相互矛盾的要求;在这些情况下,ECMAScript 优先。此外,在某些情况下,GraalJS 并非有意完全复制 Nashorn 功能,例如出于安全原因。

解决方案

  • 启用 GraalJS 的 Nashorn 兼容模式以添加默认情况下未启用的功能——这应该可以解决大多数情况。但是,请注意,这会对应用程序安全产生负面影响!有关详细信息,请参阅 Nashorn 迁移指南

特定应用程序

  • 对于 JSR 223 ScriptEngine,您可能希望将系统属性 polyglot.js.nashorn-compat 设置为 true 以使用 Nashorn 兼容模式。
  • 对于 ant,在通过 ScriptEngine 使用 GraalJS 时,使用 ANT_OPTS 环境变量 (ANT_OPTS="-Dpolyglot.js.nashorn-compat=true")。

为什么诸如 array.map()fn.apply() 之类的内置函数在非 JavaScript 对象(例如来自 Java 的 ProxyArray)上不可用? #

原因

  • 提供给 JavaScript 的 Java 对象尽可能地与 JavaScript 对应对象一样处理。例如,提供给 JavaScript 的 Java 数组在可能的情况下被视为 JavaScript Array exotic objects(JavaScript 数组);对于函数也是如此。一个明显的区别是这些对象的原型为 null。这意味着,虽然您可以在 JavaScript 代码中读取 Java 数组的 length 以及读取和写入其值,但您无法对其调用 sort(),因为 Array.prototype 默认情况下未提供。

解决方案

  • 虽然对象没有分配原型的函数,但您可以显式地调用它们,例如 Array.prototype.call.sort(myArray)
  • 我们提供选项 js.foreign-object-prototype。启用后,JavaScript 侧的对象将获得最适用的原型集(例如 Array.prototypeFunction.prototypeObject.prototype),因此可以表现得更像各自类型的原生 JavaScript 对象。这里将应用正常的 JavaScript 优先级规则,例如,对象的自身属性(在本例中为 Java 对象的属性)优先于并隐藏来自原型的属性。

请注意,虽然可以对相应的 Java 类型调用 JavaScript 内置函数(例如来自 Array.prototype 的函数),但这些函数期望 JavaScript 语义。这意味着当 Java 中不支持操作时,操作可能会失败(通常会发生 TypeErrorMessage not supported)。以 Array.prototype.push 为例:数组可以在 JavaScript 中扩展,而在 Java 中它们是固定大小的,因此在语义上不可能进行 push 操作,并且会失败。在这种情况下,您可以包装 Java 对象并显式处理这种情况。为此,请使用 ProxyObjectProxyArray 接口。

如何验证 GraalJS 是否在我的应用程序上运行? #

如果您的模块随测试一起提供,请使用 GraalJS 执行这些测试。当然,这只会测试您的应用程序,而不是它的依赖项。您可以使用 GraalVM 语言兼容性 工具来发现您感兴趣的模块是否在 GraalJS 上进行了测试,以及它的测试是否成功通过。此外,您可以将您的 package-lock.jsonpackage.json 文件上传到该工具中,它将分析所有您的依赖项。

性能 #

为什么我的应用程序在 GraalJS 上比在其他引擎上运行得慢? #

原因

  • 确保您的基准测试考虑了预热。在最初的几次迭代中,GraalJS 可能比其他引擎慢,但在充分预热后,这种差异应该会逐渐消失。
  • GraalJS 在两种不同的独立程序中发布:原生(默认)和 JVM(带有 -jvm 后缀)。默认的原生模式提供更快的启动速度和更低的延迟,但在应用程序预热后,它可能会表现出更慢的峰值性能(更低的吞吐量)。在JVM 模式下,您的应用程序可能需要数百毫秒才能启动,但通常会表现出更好的峰值性能。
  • 通过新创建的 org.graalvm.polyglot.Context 重复执行代码速度很慢,即使每次执行的代码相同。

解决方案

  • 在您的基准测试中使用适当的预热,并忽略应用程序仍在预热的最初几次迭代。
  • 在 Java 应用程序中嵌入 GraalJS 时,请确保在 GraalVM JDK 上运行以获得最佳性能。
  • 使用 JVM 独立程序以实现更慢的启动速度,但更高的峰值性能。
  • 仔细检查您是否未设置任何可能降低性能的选项,例如 -ea/-esa
  • 通过 org.graalvm.polyglot.Context 运行代码时,请确保共享一个 org.graalvm.polyglot.Engine 对象,并将其传递给每个新创建的 Context。使用 org.graalvm.polyglot.Source 对象,并在可能的情况下缓存它们。然后,GraalVM 会在 Contexts 之间共享现有的编译代码,从而提高性能。有关详细信息和示例,请参阅 跨多个 Contexts 的代码缓存
  • 尝试将问题缩小到根本原因,并 提交问题,以便 GraalVM 团队可以查看。

如何才能获得最佳的峰值性能? #

以下是一些可以遵循的技巧来分析和提高峰值性能

  • 在测量时,请确保在开始测量峰值性能之前,已经给 Graal 编译器足够的时间来编译所有热方法。一个有用的命令行选项是 --engine.TraceCompilation=true——这会在每次编译(JavaScript)方法时输出一条消息。直到这条消息变得不那么频繁时,才开始测量。
  • 如果可能,请比较原生镜像模式和 JVM 模式之间的性能。根据应用程序的特性,其中一种模式可能显示出更好的峰值性能。
  • Polyglot API 带有几个工具和选项来检查应用程序的性能
    • --cpusampler--cputracer 将在应用程序终止时打印一个最热方法列表。使用该列表找出应用程序的大部分时间花在了哪里。
    • --experimental-options --memtracer 可以帮助您了解应用程序的内存分配。有关更多详细信息,请参阅 性能分析命令行工具

在原生镜像中运行 GraalJS 与在 JVM 中运行有什么区别? #

本质上,GraalJS 引擎是一个普通的 Java 应用程序。它可以在任何 JVM(JDK 21 或更高版本)上运行,但为了获得更好的结果,它应该是 GraalVM JDK,或者使用 Graal 编译器的兼容 Oracle JDK。这种模式使 JavaScript 引擎能够在运行时完全访问 Java,但这也要求 JVM 首先(即时)编译 JavaScript 引擎,就像任何其他 Java 应用程序一样。

在原生镜像中运行意味着 JavaScript 引擎(包括来自 JDK 的所有依赖项)都被预编译到一个本地可执行文件中。这将极大地减少任何 JavaScript 应用程序的启动时间,因为 GraalVM 可以立即开始编译 JavaScript 代码,而无需先自行编译。但是,这种模式只允许 GraalVM 访问在镜像创建时已知的 Java 类。最重要的是,这意味着 JavaScript 到 Java 的互操作性功能在这种模式下不可用,因为它们需要在运行时动态加载类和执行任意 Java 代码。

错误 #

TypeError: 无法访问主机类 com.myexample.MyClass 或者该类不存在 #

原因

  • 您正在尝试访问 js 进程未知的 Java 类,或者该类不属于您的代码可以访问的允许类。

解决方案

  • 确保类名没有拼写错误。
  • 确保该类位于类路径上。使用 --vm.cp=<classpath> 选项。
  • 确保允许访问该类,方法是在您的类上添加 @HostAccess.Export 注解,或者将 Context.Builder.allowHostAccess() 设置为允许设置。请参阅 org.graalvm.polyglot.Context

TypeError: UnsupportedTypeException #

TypeError: execute on JavaObject[Main$$Lambda$63/1898325501@1be2019a (Main$$Lambda$63/1898325501)] failed due to: UnsupportedTypeException

原因

  • 在某些情况下,GraalJS 不允许在从 JavaScript 调用 Java 时使用具体的回调类型。例如,期望 Value 对象的 Java 函数可能会因上述错误消息而失败。

解决方案

  • 更改 Java 回调方法中的签名。

状态

  • 这是一个 已知限制,应该在将来的版本中解决。

示例

import java.util.function.Function;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
import org.graalvm.polyglot.HostAccess;

public class Minified {
  public static void main(String ... args) {
    //change signature to Function<Object, String> to make it work
    Function<Value, String> javaCallback = (test) -> {
      return "passed";
    };
    try(Context ctx = Context.newBuilder()
    .allowHostAccess(HostAccess.ALL)
    .build()) {
      Value jsFn = ctx.eval("js", "f => function() { return f(arguments); }");
      Value javaFn = jsFn.execute(javaCallback);
      System.out.println("finished: "+javaFn.execute());
    }
  }
}

TypeError: 消息不支持 #

TypeError: execute on JavaObject[Main$$Lambda$62/953082513@4c60d6e9 (Main$$Lambda$62/953082513)] failed due to: Message not supported.

原因

  • 您正在尝试对一个多语言对象执行一个操作(消息),但该对象不支持此操作。例如,您正在对一个不可执行对象调用 Value.execute()
  • 安全设置(例如 org.graalvm.polyglot.HostAccess)可能会阻止操作。

解决方案

  • 确保相关对象(类型)支持相应的消息。
  • 具体来说,请确保您尝试在 Java 类型上执行的 JavaScript 操作在语义上是可能的。例如,虽然您可以在 JavaScript 中将值推送到数组中,从而自动扩展数组,但 Java 中的数组是固定长度的,尝试将值推送到 Java 数组会导致“消息不支持”错误。您可能需要为这种情况包装 Java 对象,例如作为 ProxyArray
  • 确保允许访问该类,方法是在您的类上添加 @HostAccess.Export 注解,或者将 Context.Builder.allowHostAccess() 设置为允许设置。请参阅 org.graalvm.polyglot.Context
  • 您是否尝试调用 Java Lambda 表达式或函数式接口?用 @HostAccess.Export 注解适当的方法可能会成为一个陷阱。虽然您可以注解函数式接口引用的方法,但接口本身(或在后台创建的 Lambda 类)无法正确注解并被识别为“导出”。请参阅以下突出显示问题和可行解决方案的示例。

一个在某些 HostAccess 设置(例如 HostAccess.EXPLICIT)下会触发“消息不支持”错误的示例

{
  ...
  //a JS function expecting a function as argument
  Value jsFn = ...;
  //called with a functional interface as argument
  jsFn.execute((Function<Integer, Integer>)this::javaFn);
  ...
}

@Export
public Object javaFn(Object x) { ... }

@Export
public Callable<Integer> lambda42 = () -> 42;

在上面的示例中,方法 javaFn 似乎用 @Export 注解了,但传递给 jsFn 的函数式接口 没有 注解,因为函数式接口就像 javaFn 的包装器,因此隐藏了注解。lambda42 也没有正确注解,这种模式注解的是 字段 lambda42,而不是生成的 lambda 类中的可执行函数。

要将 @Export 注解添加到函数式接口,请改用以下模式

import java.util.function.Function;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
import org.graalvm.polyglot.HostAccess;

public class FAQ {
  public static void main(String[] args) {
    try(Context ctx = Context.newBuilder()
    .allowHostAccess(HostAccess.EXPLICIT)
    .build()) {
      Value jsFn = ctx.eval("js", "f => function() { return f(arguments); }");
      Value javaFn = jsFn.execute(new MyExportedFunction());
      System.out.println("finished: " + javaFn.execute());
    }
  }

  @FunctionalInterface
  public static class MyExportedFunction implements Function<Object, String> {
    @Override
    @HostAccess.Export
    public String apply(Object s) {
      return "passed";
    }
  };
}

另一种选择是允许访问 java.function.Functionapply 方法。但是,请注意,这允许访问该接口的 所有 实例,在大多数生产环境中,这将过于宽松并打开潜在的安全漏洞。

HostAccess ha = HostAccess.newBuilder(HostAccess.EXPLICIT)
  //warning: too permissive for use in production
  .allowAccess(Function.class.getMethod("apply", Object.class))
  .build();

警告:实现不支持运行时编译。 #

如果您收到以下警告,则您不是在 GraalVM JDK 上运行,或者不是在使用 Graal 编译器的兼容 Oracle JDK 或 OpenJDK 上运行

[engine] WARNING: The polyglot context is using an implementation that does not support runtime compilation.
The guest application code will therefore be executed in interpreted mode only.
Execution only in interpreted mode will strongly impair guest application performance.
To disable this warning, use the '--engine.WarnInterpreterOnly=false' option or the '-Dpolyglot.engine.WarnInterpreterOnly=false' system property.

要解决此问题,请使用 GraalVM 或查看如何 在通用 JDK 上运行 GraalJS 指南,了解如何在兼容的 Graal 启用的通用 JDK 上设置 Graal 编译器。

但是,如果这是故意的,您可以通过设置上述选项来禁用警告并继续以降低的性能运行,可以通过命令行或使用 Context.Builder 来设置,例如

try (Context ctx = Context.newBuilder("js")
    .option("engine.WarnInterpreterOnly", "false")
    .build()) {
  ctx.eval("js", "console.log('Greetings!');");
}

请注意,当使用显式多语言引擎时,必须在 Engine 上设置选项,例如

try (Engine engine = Engine.newBuilder()
    .option("engine.WarnInterpreterOnly", "false")
    .build()) {
  try (Context ctx = Context.newBuilder("js").engine(engine).build()) {
    ctx.eval("js", "console.log('Greetings!');");
  }
}

联系我们