常见问题

以下是关于在 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 对象的属性)优先于并隐藏来自原型的属性。

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

如何验证 GraalJS 在我的应用程序上正常工作? #

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

性能 #

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

原因

  • 确保您的基准测试考虑了预热。在前几次迭代中,GraalJS 可能比其他引擎慢,但在充分预热后,这种差异应该会消除。
  • GraalJS 以两种不同的独立模式提供:Native(默认)和 JVM(带有 -jvm 后缀)。默认的 *Native* 模式提供更快的启动速度和更低的延迟,但一旦应用程序预热,它可能会表现出较慢的峰值性能(较低的吞吐量)。在 *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 会在不同上下文之间共享现有编译代码,从而提高性能。有关更多详细信息和示例,请参阅跨多个上下文的代码缓存
  • 尝试将问题归结为根本原因并提交问题,以便 GraalVM 团队进行调查。

如何实现最佳峰值性能? #

以下是您可以遵循的一些分析和改进峰值性能的技巧

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

在 Native Image 中运行 GraalJS 与在 JVM 中运行有什么区别? #

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

在 Native Image 中运行意味着 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

原因

  • 在某些情况下,当从 JavaScript 调用 Java 时,GraalJS 不允许具体的 callback 类型。例如,一个期望 Value 对象的 Java 函数可能会因此而失败,并出现引用的错误消息。

解决方案

  • 更改 Java callback 方法中的签名。

状态

示例

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: Message not supported #

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 操作在 Java 中语义上是可行的。例如,虽然您可以在 JavaScript 中将值 push 到数组中并自动增长数组,但 Java 中的数组是固定长度的,尝试 push 到 Java 数组将导致 Message not supported 失败。对于此类情况,您可能需要包装 Java 对象,例如包装为 ProxyArray
  • 通过在您的类上添加 @HostAccess.Export 注解和/或将 Context.Builder.allowHostAccess() 设置为允许模式,确保允许访问该类。请参阅 org.graalvm.polyglot.Context
  • 您是否正在尝试调用 Java Lambda 表达式或函数式接口?使用 @HostAccess.Export 注解标注正确的方法可能会是一个陷阱。虽然您可以标注函数式接口引用的方法,但接口本身(或后台创建的 Lambda 类)未能被正确标注并识别为已导出。请参见下文示例,了解问题所在及可行的解决方案。

一个在某些 HostAccess 设置下(例如 HostAccess.EXPLICIT)触发 Message not supported 错误的示例

{
  ...
  //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!');");
  }
}

联系我们