GraalVM-应用提升指南-全-
GraalVM 应用提升指南(全)
原文:
zh.annas-archive.org/md5/f3da6d62d13eb2ba8f5234075a237c8e译者:飞龙
前言
GraalVM 是一种通用的虚拟机,允许程序员将用 JVM 语言(如 Java、Kotlin 和 Groovy)编写的应用程序嵌入、编译、互操作和运行;非 JVM 语言(如 JavaScript、Python、WebAssembly、Ruby 和 R);以及 LLVM 语言(如 C 和 C++)。
GraalVM 提供了 Graal 即时(JIT)编译器,这是 Java 虚拟机编译器接口(JVMCI)的一个实现,它完全基于 Java,并使用 Java JIT 编译器(C2 编译器)优化技术作为基础,并在此基础上构建。Graal JIT 编译器比 Java C2 JIT 编译器更复杂。GraalVM 是 JDK 的直接替代品,这意味着所有目前在 JDK 上运行的应用程序都应该在 GraalVM 上运行,而无需更改任何应用程序代码。
GraalVM 还提供了即时编译(AOT)功能,以静态链接构建原生镜像。GraalVM AOT 编译有助于构建具有非常小体积、快速启动和执行速度的原生镜像,这对于现代微服务架构来说非常理想。
虽然 GraalVM 是基于 Java 构建的,但它不仅支持 Java,还使 JavaScript、Python、R、Ruby、C 和 C++ 等多种语言能够进行多语言开发。它提供了一个可扩展的框架 Truffle,允许任何语言在平台上构建和运行。
GraalVM 正在成为运行云原生 Java 微服务的默认运行时。很快,所有 Java 开发者都将使用 GraalVM 来运行他们的云原生 Java 微服务。市场上已经出现了许多基于 GraalVM 的微服务框架,例如 Quarkus、Micronaut、Spring Native 等。
与 Java 一起工作的开发者将能够通过这本关于 GraalVM 和云原生微服务 Java 框架的实用指南来运用他们的知识。本书提供了实施和相关方法的动手实践方法,让您迅速上手并高效工作。本书还通过简单易懂的示例逐步解释了基本概念。
本书是针对希望优化其应用程序性能并寻求解决方案的开发者的实用指南。我们将从简要介绍 GraalVM 架构和底层工作原理开始。开发者将迅速进入探索在 GraalVM 上运行 Java 应用程序所能获得性能优势的阶段。我们将学习如何创建原生镜像,并了解 AOT 如何显著提高应用程序性能。然后,我们将探索构建多语言应用程序的示例,并探讨在同一虚拟机上运行的语言之间的互操作性。我们将探索 Truffle 框架,以实现我们自己的语言在 GraalVM 上最优运行。最后,我们还将了解 GraalVM 在云原生和微服务开发中的具体益处。
本书面向对象
本书的主要受众是希望优化其应用程序性能的 JVM 开发者。对于探索使用 Python/R/Ruby/Node.js 生态系统中的工具开发多语言应用程序的 JVM 开发者来说,本书也非常有用。由于本书面向经验丰富的开发者/程序员,读者必须熟悉软件开发概念,并且应该对使用编程语言有良好的了解。
本书涵盖内容
第一章, Java 虚拟机的发展历程,回顾了 JVM 的发展历程以及它是如何优化解释器和编译器的。它将介绍 C1 和 C2 编译器,以及 JVM 为了使 Java 程序运行更快而执行的代码优化类型。
第二章, JIT、HotSpot 和 GraalJIT,深入探讨了 JIT 编译器和 Java HotSpot 的工作原理以及 JVM 如何在运行时优化代码。
第三章, GraalVM 架构,提供了 Graal 和各个架构组件的架构概述。章节深入探讨了 GraalVM 的工作原理以及它是如何为多种语言实现提供单个虚拟机的。本章还涵盖了 GraalVM 在标准 JVM 之上带来的优化。
第四章, Graal 即时编译器,讨论了 GraalVM 的 JIT 编译选项。详细介绍了 Graal JIT 编译器执行的各项优化。随后是一个实战教程,讲解如何使用各种编译器选项来优化执行。
第五章, Graal 预编译器及原生图像,是一个实战教程,讲解如何构建原生图像,并使用配置文件引导优化技术对这些图像进行优化和运行。
第六章, 松露 - 概述,介绍了 Truffle 的多语言互操作性功能和高级框架组件。它还涵盖了如何在运行在 GraalVM 上的不同语言编写的应用程序之间传输数据。
第七章, GraalVM 多语言 - JavaScript 和 Node.js,介绍了 JavaScript 和 NodeJs。随后是一个教程,讲解如何使用多语言 API 实现互操作性,以便在示例 JavaScript 和 NodeJS 应用程序与 Python 应用程序之间进行交互。
第八章, GraalVM 多语言 - Truffle 上的 Java、Python 和 R,介绍了 Python、R 和 Truffle(Espresso)上的 Java。随后是一个教程,讲解如何使用多语言 API 实现各种语言之间的互操作性。
第九章,GraalVM 多语言 – LLVM、Ruby 和 WASM,介绍了 JavaScript 和 Node.js。随后是一个教程,说明如何使用 Polyglot API 在示例 JavaScript/Node.js 应用程序之间进行互操作。
第十章,使用 GraalVM 的微服务架构,涵盖了现代微服务架构以及新框架如 Quarkus 和 Micronaut 如何实现 Graal 以达到最优化微服务架构。
要充分利用本书
本书是一本实践指南,提供了如何使用 GraalVM 的逐步说明。在整个书中,作者使用了非常简单、易于理解的代码示例,这些示例将帮助您理解 GraalVM 的核心概念。所有代码示例都提供在 Git 仓库中。您应具备良好的 Java 编程语言知识。本书还涉及 Python、JavaScript、Node.js、Ruby 和 R,但示例故意保持简单,以便理解,同时专注于展示多语言互操作性概念。

如果您使用的是本书的数字版,我们建议您亲自输入代码或通过 GitHub 仓库(下一节中提供链接)访问代码。这样做将帮助您避免与代码复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件:github.com/PacktPublishing/Supercharge-Your-Applications-with-GraalVM。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有其他来自我们丰富的书籍和视频目录的代码包,可在 github.com/PacktPublishing/ 上找到。查看它们吧!
代码在行动
代码在行动视频可以在 bit.ly/3eM5ewO 上查看。
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:static.packt-cdn.com/downloads/9781800564909_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。以下是一个示例:“在 Truffle 中,它是一个从 com.oracle.truffle.api.nodes.Node 派生的 Java 类。”
代码块设置如下:
@Fallback protected void typeError (Object left, Object right) {
throw new TypeException("type error: args must be two integers or floats or two", this);
}
任何命令行输入或输出都应如下编写:
✗/Library/Java/JavaVirtualMachines/graalvm-ee-java11-21.0.0.2/Contents/Home/bin/npm --version
6.14.10
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“从管理面板中选择系统信息。”
小贴士或重要注意事项
看起来像这样。
联系我们
读者反馈始终受欢迎。
customercare@packtpub.com。
勘误表:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告这一点,我们将不胜感激。请访问 www.packtpub.com/support/errata,选择您的书籍,点击勘误表提交表单链接,并输入详细信息。
copyright@packt.com 与材料链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用了这本书,为何不在您购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
想了解更多关于 Packt 的信息,请访问 packt.com。
第一章:第一部分:JVM 的发展
本节将介绍 JVM 的发展历程,以及它是如何优化解释器和编译器的。它将介绍 C1 和 C2 编译器,以及 JVM 为了使 Java 程序运行更快所执行的代码优化类型。
本节包括以下章节:
-
第一章, Java 虚拟机的发展
-
第二章, JIT、HotSpot 和 GraalJIT
第二章:Java 虚拟机演变
本章将向您介绍 Java 虚拟机(JVM)的演变过程以及它是如何优化解释器和编译器的。我们将了解 C1 和 C2 编译器以及 JVM 执行的多种代码优化类型,以使 Java 程序运行得更快。
本章将涵盖以下主题:
-
GraalVM 简介
-
学习 JVM 的工作原理
-
理解 JVM 架构
-
理解 JVM 使用 即时编译器(JIT)执行的优化类型
-
学习 JVM 方法的优缺点
到本章结束时,您将对 JVM 架构有一个清晰的理解。这对于理解 GraalVM 架构以及 GraalVM 如何进一步优化并建立在 JVM 最佳实践之上至关重要。
技术要求
本章不需要任何特定的软件/硬件。
GraalVM 简介
GraalVM 是一个高性能虚拟机,为现代云原生应用程序提供运行时。云原生应用程序是基于服务架构构建的。微服务架构改变了构建微应用程序的范式,这挑战了构建和运行应用程序的基本方式。微服务运行时需要一套不同的要求。
这里是一些基于微服务架构构建的云原生应用程序的关键要求:
-
更小的内存占用:云原生应用程序运行在“按使用付费”的模式上。这意味着云原生运行时需要具有更小的内存占用,并且应该以最佳 CPU 循环运行。这将有助于使用更少的云资源运行更多的工作负载。
-
快速启动:可扩展性是容器化微服务架构最重要的方面之一。应用程序启动越快,它就能更快地扩展集群。这对于无服务器架构来说尤为重要,在无服务器架构中,代码在请求初始化和运行后关闭。
-
多语言和互操作性:多语言是现实;每种语言都有其优势,并将继续如此。云原生微服务正在用不同的语言构建。拥有一个能够接纳多语言需求并提供跨语言互操作性的架构非常重要。随着我们转向现代架构,尽可能重用代码和逻辑至关重要,这些代码和逻辑是经过时间考验的,对业务至关重要。
GraalVM 为所有这些需求提供了解决方案,并为嵌入和运行多语言云原生应用程序提供了一个通用平台。它是基于 JVM 构建的,并带来了进一步的优化。在了解 GraalVM 的工作原理之前,了解 JVM 的内部工作原理非常重要。
传统的 JVM(在 GraalVM 之前)已经发展成为最成熟的运行时实现。虽然它具有之前列出的某些要求,但它并不是为云原生应用程序设计的,并且带有单体设计原则的包袱。它不是云原生应用程序的理想运行时。
本章将详细介绍 JVM 的工作原理以及 JVM 架构的关键组件。
学习 JVM 的工作原理
Java 是最成功和最广泛使用的语言之一。Java 之所以非常成功,是因为其“一次编写,到处运行”的设计原则。JVM 通过位于应用程序代码和机器码之间,将应用程序代码解释为机器码来实现这一设计原则。
传统上,运行应用程序代码有两种方式:
-
编译器:应用程序代码直接编译为机器码(C、C++等)。编译器通过构建过程将应用程序代码转换为机器码。编译器为特定的目标架构生成最优化代码。应用程序代码必须编译为目标架构。一般来说,编译的代码总是比解释的代码运行得快,并且可以在编译时而不是运行时识别代码语义问题。
-
解释器:应用程序代码逐行解释为机器码(JavaScript 等)。由于解释器逐行运行,代码可能没有针对目标架构进行优化,并且运行速度较慢,与编译的代码相比。解释器具有“一次编写,到处运行”的灵活性。一个很好的例子是主要用于 Web 应用程序的 JavaScript 代码。它在不同的目标浏览器上几乎不需要或只需要很少的更改即可运行。解释器通常运行较慢,适用于运行小型应用程序。
JVM 结合了解释器和编译器的优点。以下图表说明了 JVM 如何使用解释器和编译器方法运行 Java 代码:

图 1.1 – Java 编译器和解释器
让我们看看它是如何工作的:
-
Java 编译器(javac)将 Java 应用程序源代码编译为字节码(中间格式)。
-
JVM 在运行时逐行将字节码解释为机器码。这有助于将优化的字节码转换为目标机器码,从而帮助在不同的目标机器上运行相同的应用程序代码,而无需重新编程或重新编译。
-
JVM 还有一个即时编译器(JIT),通过分析代码在运行时进一步优化代码。
在本节中,我们探讨了 Java 编译器和 JIT 如何协同工作,在 JVM 上以更高的层次运行 Java 代码。在下一节中,我们将学习 JVM 的架构。
理解 JVM 架构
经过多年的发展,JVM 已经发展成为最成熟的 VM 运行时。它具有非常结构化和复杂的运行时实现。这也是 GraalVM 被构建出来以利用 JVM 的所有最佳特性,并为云原生世界提供进一步优化所需的功能的原因之一。为了更好地欣赏 GraalVM 架构及其在 JVM 之上带来的优化,了解 JVM 架构是非常重要的。
本节将详细向您介绍 JVM 架构。以下图显示了 JVM 中各个子系统的整体架构:

图 1.2 – JVM 的整体架构
本节的其余部分将详细介绍这些子系统。
类加载器子系统
类加载器子系统负责分配所有相关的.class文件并将这些类加载到内存中。类加载器子系统还负责在类初始化和加载到内存之前链接和验证.class文件的规范。类加载器子系统具有以下三个关键功能:
-
加载
-
链接
-
初始化
以下图显示了类加载器子系统的各个组件:

图 1.3 – 类加载器子系统的组件
让我们现在看看每个组件的作用。
加载
在传统的基于编译器的语言(如 C/C++)中,源代码被编译成目标代码,然后所有依赖的目标代码在构建最终可执行文件之前通过链接器链接。所有这些都属于构建过程的一部分。一旦构建了最终的可执行文件,它就会被加载器加载到内存中。Java 的工作方式不同。
Java 源代码(.java)由 Java 编译器(javac)编译成字节码(.class)文件。类加载器是 JVM 的关键子系统之一,负责加载运行应用程序所需的所有依赖类。这包括应用程序开发者编写的类、库以及Java 软件开发工具包(SDK)类。
该系统包括三种类型的类加载器:
-
rt.jar,其中包含所有 Java 标准版 JDK 类,如java.lang、java.net、java.util和java.io。引导加载器负责加载运行任何 Java 应用程序所需的所有类。这是 JVM 的核心部分,用本地语言实现。 -
jre/lib/ext目录。扩展类加载器类通常是 Java 中实现的引导扩展类。扩展类加载器用 Java 实现(sun.misc.Launcher$ExtClassLoader.class)。 -
CLASSPATH环境变量)。这也用 Java 实现(sun.misc.Launcher$AppClassLoader.class)。
引导、扩展和应用程序类加载器负责加载运行应用程序所需的所有类。在类加载器找不到所需类的情况下,将抛出ClassNotFoundException。
类加载器实现了委托层次算法。以下图表显示了类加载器如何实现委托层次算法来加载所有必需的类:

图 1.4 – 类加载器委托层次算法实现流程图
让我们了解这个算法是如何工作的:
-
JVM 将在方法区中查找类(将在本节稍后详细讨论)。如果没有找到类,它将要求应用程序类加载器将类加载到内存中。
-
应用程序类加载器将调用委托给扩展类加载器,扩展类加载器再委托给引导类加载器。
-
引导类加载器将在引导
CLASSPATH中查找类。如果找到类,它将加载到内存中。如果没有找到类,控制权将委托给扩展类加载器。 -
扩展类加载器将尝试在扩展
CLASSPATH中查找类。如果找到类,它将加载到内存中。如果没有找到类,控制权将委托给应用程序类加载器。 -
应用程序类加载器将尝试在
CLASSPATH中查找类。如果找不到,它将抛出ClassNotFoundException,否则,该类将被加载到方法区,JVM 将开始使用它。
链接
一旦类被加载到内存中(到方法区,在内存子系统部分将进一步讨论),类加载器子系统将执行链接。链接过程包括以下步骤:
-
java.lang.Object。验证阶段验证并确保方法运行时没有任何问题。 -
准备:一旦所有类都加载并验证,JVM 将为类变量(静态变量)分配内存。这也包括调用静态初始化(静态块)。
-
解析:JVM 通过定位符号表中引用的类、接口、字段和方法来进行解析。JVM 可能在初始验证(静态解析)期间解析符号,也可能在类正在验证时解析(懒解析)。
类加载器子系统会抛出各种异常,包括以下内容:
-
ClassNotFoundException -
NoClassDefFoundError -
ClassCastException -
UnsatisfiedLinkError -
ClassCircularityError -
ClassFormatError -
ExceptionInInitializerError
您可以参考 Java 规范以获取更多详细信息:docs.oracle.com/en/java/javase。
初始化
一旦所有类都被加载并且符号被解析,初始化阶段就开始了。在这个阶段,类被初始化(new)。这包括初始化静态变量、执行静态块和调用反射方法(java.lang.reflect)。这也可能导致加载那些类。
类加载器在应用程序运行之前将所有类加载到内存中。大多数情况下,类加载器必须加载完整的类层次结构和依赖类(尽管存在延迟解析),以验证方案。这既耗时又占用大量内存。如果应用程序使用反射并且需要加载反射类,那么这个过程会更慢。
在了解类加载器子系统之后,现在让我们来理解内存子系统是如何工作的。
内存子系统
内存子系统是 JVM 中最关键的子系统之一。正如其名称所暗示的,内存子系统负责管理方法变量、堆栈、栈和寄存器分配的内存。以下图表显示了内存子系统的架构:

图 1.5 – 内存子系统架构
内存子系统有两个区域:JVM 级别和线程级别。让我们详细讨论每个区域。
JVM 级别
JVM 级别的内存,正如其名称所暗示的,是对象在 JVM 级别存储的地方。这不是线程安全的,因为多个线程可能会访问这些对象。这也解释了为什么当程序员在这个区域更新对象时,建议他们编写线程安全的(同步)代码。JVM 级别的内存有两个区域:
-
方法:方法区是存储所有类级别数据的地方。这包括类名、层次结构、方法、变量和静态变量。
-
堆:堆是存储所有对象和实例变量的地方。
线程级别
线程级别的内存是存储所有线程局部对象的地方。这对相应的线程是可访问/可见的,因此它是线程安全的。线程级别的内存有三个区域:
-
栈:对于每个方法调用,都会创建一个栈帧,用于存储所有方法级别的数据。栈帧包括在方法作用域内创建的所有变量/对象、操作数栈(用于执行中间操作)、帧数据(存储与方法对应的所有符号)以及异常捕获块信息。
-
寄存器:PC 寄存器跟踪指令执行并指向正在执行的当前指令。这为每个正在执行的线程维护。
-
本地方法栈:本地方法栈是一种特殊的栈,用于存储本地方法信息,这在调用和执行本地方法时非常有用。
现在类已经被加载到内存中,让我们看看 JVM 执行引擎是如何工作的。
JVM 执行引擎子系统
JVM 执行引擎是 JVM 的核心,所有执行都在这里发生。这是字节码被解释和执行的地方。JVM 执行引擎使用内存子系统来存储和检索对象。JVM 执行引擎有三个关键组件,如下所示:

图 1.6 – JVM 执行引擎架构
我们将在接下来的章节中详细讨论每个组件。
字节码解释器
如本章前面所述,字节码(.class)是 JVM 的输入。JVM 字节码解释器从 .class 文件中选取每条指令,将其转换为机器码并执行。解释器的明显缺点是它们没有被优化。指令按顺序执行,即使同一个方法被多次调用,它也会逐条指令执行,解释后再执行。
JIT 编译器
JIT 编译器通过分析由解释器执行的代码,识别出代码可以优化的区域,并将它们编译为目标机器码,以便它们可以更快地执行。字节码和编译代码片段的组合提供了执行类文件的最佳方式。
下面的图示详细说明了 JVM 的工作原理,以及 JVM 使用的各种类型的 JIT 编译器来优化代码:

图 1.7 – JVM 与 JIT 编译器的详细工作原理
让我们理解前面图示中显示的工作原理:
-
JVM 解释器逐个字节码执行,并使用机器码对其进行解释,利用字节码到机器码的映射。
-
JVM 使用计数器持续分析代码,以统计代码执行的次数,如果计数器达到阈值,它将使用 JIT 编译器编译该代码以进行优化,并将其存储在代码缓存中。
-
JVM 然后检查该编译单元(块)是否已经编译。如果 JVM 在代码缓存中找到已编译的代码,它将使用这些已编译的代码以实现更快的执行。
-
JVM 使用两种类型的编译器,C1 编译器和 C2 编译器,来编译代码。
如 图 1.7 所示,JIT 编译器通过分析正在运行的代码进行优化,并在一段时间内识别出可以编译的代码。JVM 运行编译后的代码片段,而不是解释代码。这是一种运行解释代码和编译代码的混合方法。
JVM 引入了两种类型的编译器,C1(客户端)和 C2(服务器),而 JVM 的最新版本则结合了两者在运行时优化和编译代码的最佳性能。让我们更好地理解这些类型:
-
C1 编译器:引入了一个性能计数器,用于计算特定方法/代码片段执行的次数。一旦方法/代码片段被使用特定次数(阈值),则该代码片段将由 C1 编译器编译、优化和缓存。下次调用该代码片段时,它将直接从缓存中执行编译后的机器指令,而不是通过解释器。这引入了第一级优化。
-
C2 编译器:在代码执行过程中,JVM 将执行运行时代码分析,确定代码路径和热点。然后运行 C2 编译器以进一步优化热点代码路径。这也被称为热点。
C1 编译器更快,适合短运行时应用,而 C2 编译器较慢且重量级,但非常适合长时间运行的过程,如守护进程和服务器,因此代码在长时间运行中表现更佳。
在 Java 6 中,有一个命令行选项可以用来选择使用 C1 或 C2 方法(使用命令行参数 -client(用于 C1)和 -server(用于 C2))。在 Java 7 中,有一个命令行选项可以同时使用两者。自 Java 8 以来,C1 和 C2 编译器都用于优化,作为默认行为。
编译有五个层级/级别。可以通过生成编译日志来了解哪个 Java 方法使用了哪个编译器层级/级别。以下为五个编译层级/级别:
-
解释代码(级别 0)
-
简单的 C1 编译代码(级别 1)
-
有限的 C1 编译代码(级别 2)
-
完整的 C1 编译代码(级别 3)
-
C2 编译代码(级别 4)
现在我们来看看 JVM 在编译过程中应用的各类代码优化。
代码优化
JIT 编译器生成正在编译的代码的内部表示,以理解其语义和语法。这些内部表示是树形数据结构,JIT 将在这些结构上运行代码优化(作为多个线程,可以通过命令行的 XcompilationThreads 选项进行控制)。
以下是一些 JIT 编译器对代码执行的优化:
-
-XX:MaxFreqInlineSize标志(默认值为 325 字节)。 -
逃逸分析:JVM 对变量进行配置以分析变量的使用范围。如果变量没有超出局部作用域,它将执行局部优化。锁消除就是这样一种优化,其中 JVM 决定是否真的需要为变量使用同步锁。同步锁对处理器来说非常昂贵。JVM 还决定将对象从堆移动到栈上。这有助于提高内存使用率和垃圾收集效率,因为对象在方法执行完毕后就会被销毁。
-
取消优化:取消优化是另一种关键的优化技术。JVM 在优化后对代码进行分析,并可能决定取消优化代码。取消优化将对性能产生短暂的影响。JIT 编译器在两种情况下决定取消优化:
a. 非进入代码:这在继承类或接口实现中非常明显。JIT 可能已经针对层次结构中的特定类进行了优化,但随着时间的推移,当它了解到不同的情况时,它将取消优化并针对更具体的类实现进行进一步优化。
b. 僵尸代码:在非进入代码分析期间,一些对象被垃圾收集,导致可能永远不会被调用的代码。这种代码被标记为僵尸代码。此代码将从代码缓存中删除。
除了这个之外,JIT 编译器还执行其他优化,例如控制流优化,这包括重新排列代码路径以提高效率,并将本地代码生成到目标机器代码以实现更快的执行。
JIT 编译器的优化是在一段时间内进行的,这对于长时间运行的过程很有好处。我们将在第二章**,JIT、Hotspot 和 GraalVM中详细解释 JIT 编译。
Java 预编译
预编译选项是在 Java 9 中通过 jaotc 引入的,它可以将 Java 应用程序代码直接编译成最终机器代码。代码被编译为目标架构,因此它不是可移植的。
Java 支持在 x86 架构上同时运行 Java 字节码和 AOT 编译的代码。以下图表说明了它是如何工作的。这是 Java 可以生成的最优化代码:
![图 1.8 – JVM JIT 时间编译器和预编译器的详细工作原理]

图 1.8 – JVM JIT 时间编译器和预编译器的详细工作原理
字节码将通过之前解释的方法(C1、C2)。jaotc 预先编译最常用的 Java 代码(如库)成机器代码,并将其直接加载到代码缓存中。这将减少 JVM 的负载。Java 字节码将通过常规解释器,并使用代码缓存中的代码(如果可用)。这将大大减少 JVM 在运行时编译代码的负载。通常,最常用的库可以预先编译以实现更快的响应。
垃圾收集器
Java 的一个复杂之处在于其内置的内存管理。在 C/C++ 等语言中,程序员需要负责分配和释放内存。在 Java 中,JVM 负责清理未引用的对象并回收内存。垃圾收集器是一个守护线程,它自动执行清理工作,也可以由程序员调用(System.gc() 和 Runtime.getRuntime().gc())。
本地子系统
Java 允许程序员访问原生库。原生库通常是那些为特定目标架构构建(使用如 C/C++ 等语言)并使用的库。Java 原生接口(JNI)提供了一个抽象层和接口规范,用于实现访问原生库的桥梁。每个 JVM 都为特定的目标系统实现了 JNI。程序员还可以使用 JNI 来调用原生方法。以下图表说明了原生子系统的组件:

图 1.9 – 原生子系统架构
原生子系统提供了访问和管理原生库的实现。
JVM 已经发展并拥有语言 VM 运行时最复杂的实现之一。
摘要
在本章中,我们首先学习了 GraalVM 是什么,然后了解了 JVM 的工作原理及其架构,包括其各种子系统和组件。稍后,我们还学习了 JVM 如何结合解释器和编译器的最佳方法在多种目标架构上运行 Java 代码,以及代码是如何通过 C1 和 C2 编译器即时编译的。最后,我们学习了 JVM 执行的各种类型的代码优化。
本章为我们提供了对 JVM 架构的良好理解,这将帮助我们了解 GraalVM 架构的工作原理以及它是如何建立在 JVM 之上的。
下一章将涵盖 JIT 编译器的工作原理的细节,并帮助您了解 Graal JIT 是如何建立在 JVM JIT 之上的。
问题
-
为什么 Java 代码会被解释成字节码,并在运行时编译?
-
JVM 如何加载适当的类文件并将它们链接起来?
-
JVM 中有哪些不同类型的内存区域?
-
C1 编译器和 C2 编译器之间的区别是什么?
-
JVM 中的代码缓存是什么?
-
立即执行时执行的各种代码优化类型有哪些?
进一步阅读
-
《JVM 语言入门》,作者 Vincent van der Leun,Packt 出版公司 (
www.packtpub.com/product/introduction-to-jvm-languages/9781787127944) -
《Java 文档和规范》,作者 Oracle (
docs.oracle.com/en/java/)
第三章:JIT、HotSpot 和 GraalJIT
在上一章中,我们学习了 C1 和 C2 编译器以及 C2 编译器在运行时执行的代码优化和去优化类型。
在本章中,我们将深入探讨 C2 即时编译,并介绍 Graal 的即时编译。即时(JIT)编译是 Java 能够与传统编译时(AOT)编译器竞争的关键创新之一。正如我们在上一章所学,JIT 编译随着 JVM 中的 C2 编译器而发展。C2 JIT 编译器持续分析代码执行情况,并在运行时应用各种优化和去优化,以编译/重新编译代码。
本章将是一个实战环节,我们将分析一个示例代码,了解 C2 JIT 编译器的工作原理,并介绍 Graal JIT。
在本章中,我们将涵盖以下主题:
-
了解 JIT 编译器的工作原理
-
通过识别热点区域来了解 JIT 如何优化代码
-
使用分析工具演示 JIT 编译器的工作原理
-
了解 GraalVM JIT 在 JVM JIT 之上是如何工作的
到本章结束时,你将清楚地了解 JIT 编译器的内部工作原理以及 GraalVM 如何进一步扩展它。我们将使用示例 Java 代码和 JITWatch 等分析工具来深入了解 JIT 的工作方式。
技术要求
要遵循本章中给出的说明,你需要以下工具:
-
本章中提到的所有源代码都可以从
github.com/PacktPublishing/Supercharge-Your-Applications-with-GraalVM/tree/main/Chapter02下载。 -
Maven (
maven.apache.org/install.html) -
OpenSDK (
openjdk.java.net/) 和 JavaFX (openjfx.io/) -
本章的“代码实战”视频可以在
bit.ly/3w7uWlu找到。
设置环境
在本节中,我们将设置所有必需的先决工具和环境,以便继续学习本章的其余部分。
安装 OpenJDK Java
你可以从openjdk.java.net/install/安装 OpenJDK。此 URL 提供了安装 OpenJDK 的详细说明。我们还需要 JavaFX。请参阅openjfx.io/以获取有关安装 JavaFX 的更多详细信息。
安装 JITWatch
JITWatch 是用于理解 JIT 编译器行为的最广泛使用的日志分析和可视化工具之一。它也广泛应用于代码分析和识别性能调优的机会。
JITWatch 是一个活跃的开源项目,托管在github.com/AdoptOpenJDK/jitwatch。
安装 JITWatch 的典型命令如下:
git clone git@github.com:AdoptOpenJDK/jitwatch.git
cd jitwatch
mvn clean install -DskipTests=true
./launchUI.sh
深入探讨 HotSpot 和 C2 JIT 编译器
在上一章中,我们回顾了 JVM 的演变过程以及 C2 JIT 编译器的进化。在本节中,我们将更深入地探讨 JVM C2 JIT 编译器。通过示例代码,我们将了解 JIT 编译器在运行时执行的优化。为了更好地理解 Graal JIT 编译器,了解 C2 JIT 编译器的工作原理非常重要。
基于配置文件指导的优化是 JIT 编译器的关键原则。虽然 AOT 编译器可以优化静态代码,但大多数情况下,这还不够好。了解应用程序的运行时特性以识别优化机会非常重要。JVM 内置了一个分析器,可以动态地对应用程序进行配置,以分析一些关键参数并识别优化机会。一旦识别出这些机会,它将编译这些代码为本地语言,并从运行解释代码切换到更快编译的代码。这些优化基于分析以及 JVM 做出的有根据的假设。如果这些假设中的任何一个是不正确的,JVM 将取消优化并切换回运行解释代码。这被称为混合模式执行。
下图展示了 JVM 如何执行基于配置文件指导的优化以及在执行模式之间切换的流程:
![图 2.1 – JIT 编译
![图片 B16878_Figure_2.01.jpg]
图 2.1 – JIT 编译
Java 源代码(.java)被编译成字节码(.class),这是代码的中间表示形式。JVM 使用内置的解释器开始运行字节码。解释器使用字节码到机器码的映射,逐条将字节码指令转换为机器码,然后执行它。
当 JVM 执行这些指令时,它还会跟踪一个方法被调用的次数。当某个特定方法的调用次数超过编译器的阈值时,它会启动一个编译器,在单独的编译线程上编译该方法。JVM 使用两种类型的编译器来编译代码:C1(客户端)和 C2(服务器)JIT 编译器。编译后的代码存储在代码缓存中,以便下次调用该方法时,JVM 将直接从代码缓存中执行代码,而不是进行解释。JIT 编译器对代码执行各种优化,因此随着时间的推移,应用程序的性能会得到提升。本节剩余部分将详细介绍这些组件。
代码缓存
代码缓存是 JVM 中存储编译后的本地方法(也称为 nmethod)的区域。代码缓存被设置为静态大小,经过一段时间后可能会满。一旦代码缓存满,JVM 就无法编译或存储更多代码。对代码缓存进行优化调整对于最佳性能至关重要。四个关键参数帮助我们微调 JVM 性能,以获得最佳代码缓存:
-
-XX:InitialCodeCacheSize:代码缓存的初始大小。默认大小为 160 KB(根据 JVM 版本的不同而有所变化)。 -
-XX:ReservedCodeCacheSize:代码缓存可以增长到的最大大小。默认大小为 32/48 MB。当代码缓存达到这个限制时,JVM 将抛出一个警告:“代码缓存已满。编译器已被禁用。” JVM 提供了UseCodeCacheFlushing选项,当代码缓存满时可以刷新代码缓存。当编译的代码不够热(计数器小于编译器阈值)时,代码缓存也会被刷新。 -
-XX:CodeCacheExpansionSize:这是扩展现有的值。其默认值是 32/64 KB。 -
-XX:+PrintCodeCache:此选项可用于监控代码缓存的利用率。
自 Java 9 以来,JVM 将代码缓存分为三个部分:
-
-XX:NonNMethodCodeHeapSize标志。 -
-XX:ProfiledCodeHeapSize标志。 -
-XX:NonProfiledCodeHeapSize标志。
编译器阈值
编译阈值是帮助 JVM 决定何时执行 JIT 编译的一个因素。当 JVM 检测到某个方法的执行达到编译阈值时,JVM 将启动适当的编译器进行编译(关于这一点,在本节的后面部分将详细介绍,我们将遍历各种类型的 JIT 编译器和分层编译)。
决定编译阈值基于两个关键变量。每个 JVM 都为这两个变量提供了默认值,但也可以使用适当的命令行参数进行更改。这两个变量对于调整 JVM 性能至关重要,应谨慎使用。这两个变量如下:
-
方法调用计数器:这统计了特定方法被调用的次数。
-
循环计数器:这指的是特定循环完成执行(即分支回退)的次数。有时,这也被称为回边阈值或回边计数器。
JVM 在运行时对这些两个变量进行配置,并据此决定是否需要编译该方法/循环。当达到编译阈值时,JVM 将启动一个编译线程来编译该特定方法/循环。
使用 -XX:CompilationThreshold=N 标志作为执行代码时的参数,可以更改编译阈值。对于客户端编译器,N 的默认值是 1500,而对于服务器编译器,默认值是 10000。
栈上替换
达到编译阈值的那些方法由 JIT 编译器编译,下次调用该方法时,将调用编译后的机器代码。这随着时间的推移提高了性能。然而,在长时间运行的循环达到循环计数器阈值(Backedge Threshold)的情况下,编译线程会启动代码编译。一旦循环中的代码被编译,执行将停止,并使用编译后的代码帧恢复。这个过程称为栈上替换(OSR)。让我们看看以下示例。
以下代码片段仅讨论 OSR 的工作原理。为了简化,代码仅显示一个长时间运行的循环,其中我们只是计算循环运行的总次数。在这种情况下,main()方法从未进入,因此即使达到编译阈值并且代码被编译,编译后的代码也无法使用,除非代码被替换。这就是 OSR 在优化此类代码中发挥作用的地方:
public class OSRTest {
public static void main(String[] args) {
int total = 0;
//long running loop
for(int i=0; i < 10000000; i++) {
//Perform some function
total++;
}
System.out.println("Total number of times is "+ total);
}
}
以下流程图显示了在这种情况下 OSR 是如何工作的:

图 2.2 – OSR 流程图
让我们看看它是如何工作的:
-
解释器开始执行代码。
-
当达到编译器阈值时,JVM 会启动一个编译线程来编译方法。在此期间,解释器继续执行语句。
-
一旦编译线程返回编译后的代码(编译帧),JVM 会检查解释器是否仍在执行代码。如果解释器仍在执行代码,它将暂停并执行 OSR,然后从编译后的代码开始执行。
当我们开启-XX:PrintCompilation标志运行此代码时,这是显示 JVM 执行了 OSR 的输出(%属性表示它执行了 OSR):

图 2.3 – OSR 日志截图
请参阅下一节以详细了解日志格式。
XX:+PrintCompilation
XX:+PrintCompilation是一个非常强大的参数,可以传递给 JIT 编译器以了解它们是如何启动和优化代码的。在我们用这个参数运行代码之前,让我们首先了解输出格式。
XX:+PrintCompilation生成以下格式的参数列表,参数之间用空格分隔:
<Timestamp> <CompilationID> <Flag> <Tier> <ClassName::MethodName> <MethodSize> <DeOptimization Performed if any>
这里是一个输出示例快照:

图 2.4 – 打印编译日志格式
让我们看看这些参数的含义:
-
Timestamp:这是 JVM 启动以来的毫秒时间。 -
CompilationID:这是 JVM 在编译队列中使用的内部识别号。这不一定是有序的,因为可能有后台编译线程预留了一些 ID。 -
Flags:编译器标志是非常重要的参数,会被记录下来。这表明编译器应用了哪些属性。JVM 打印出五个可能的字符的逗号分隔字符串,以指示应用给编译器的五个不同属性。如果没有应用任何属性,则显示为空字符串。这五个属性如下:a.
%字符。OSR 在本节前面已解释。此属性表明,当方法在大循环中循环时,会触发 OSR 编译。b.
!字符。这表示该方法有一个异常处理器。c.
s字符。这表示该方法被同步。d.
b字符。这表示编译是在阻塞模式下进行的。这意味着编译没有在后台进行。e.
n字符。这表示代码被编译为本地方法。 -
Tier:这表示执行了哪个编译层。有关更多详细信息,请参阅分层编译部分。 -
MethodName:此列列出正在编译的方法。 -
MethodSize:这是方法的大小。 -
Deoptimization performed:这显示了可能执行的任何去优化。我们将在下一节中详细讨论。
分层编译
在上一章中,我们简要介绍了编译层/级别。在本节中,我们将更详细地介绍。当达到编译器阈值时,客户端编译器会提前启动。服务器编译器基于分析启动。JVM 的最新版本使用两种编译器的组合来实现最佳性能。然而,用户可以使用 -client、-server 或 -d64 参数专门使用其中一个编译器。JVM 的默认行为是使用分层编译,这是最优化 JIT 编译。在分层编译中,代码首先由客户端编译器编译,然后根据分析,如果代码变得更热(因此得名 HotSpot),服务器编译器启动并重新编译代码。这个过程在上一节中通过流程图进行了说明。
分层编译随着代码的复杂化和运行时间的增加而引入了更多的优化。有些情况下,即时编译(JIT)比静态编译(AOT)运行得更优化且更快。虽然 AOT 编译引入了优化,但在构建阶段,它没有根据运行时分析来自动优化/去优化的智能。运行时分析、优化和去优化是 JIT 编译的关键优势。
有三种 JIT 编译器的版本:
-
-client参数:-XX:PrintCompilation logs the compilation process to the console. This helps in understanding how the compiler is working. -
long或double变量。这种编译器版本可以使用-server参数显式调用。 -
-d64参数。
服务器编译器在编译速度上比客户端编译器慢 4 倍。然而,它们确实生成了运行速度更快的应用程序(高达 2 倍)。
如下列出的有五个编译层级/级别。可以通过编译日志使用编译打印来查找哪些方法被编译到哪个级别。
-
Level 0 – 解释代码:这是标准的解释器模式,其中 JIT 尚未激活。JIT 的激活基于编译阈值。
-
Level 1 – 简单 C1 编译代码:这是代码的基本无配置编译。编译后的代码将没有任何配置。
-
Level 2 – 有限 C1 编译代码:在这个级别,基本的计数器被配置了。这个计数器将帮助 JVM 决定是否移动到下一个级别,即 L2。有时,当 C2 编译器忙碌时,JVM 会使用这个级别作为提升到 Level 3 的中间步骤。
-
Level 3 – 完整 C1 编译代码:在这个级别,代码被完全配置和配置。这种详细的配置将有助于决定 L4 的进一步优化。这个级别给编译器增加了 25-30%的开销,并影响了性能。
-
Level 4 – C2 编译代码:这是代码的最优化编译,其中应用了所有优化。然而,在配置时,如果 JVM 发现优化上下文已更改,它将取消优化,并用 L0 或 L1(对于简单方法)替换代码。
现在我们来看 Java HotSpot 编译器如何执行分层编译。以下图表显示了编译的各种层级和流程模式:
![Figure 2.5 – 分层编译模式
![img/B16878_Figure_2.05.jpg]
图 2.5 – 分层编译模式
让我们了解每个流程表示的含义:
-
A:这是 JVM 的正常工作模式。所有代码都从 L0 开始,当达到编译阈值时升级到 L3。在 L3,代码会进行完整的详细配置的配置。然后,当代码达到阈值时,在运行时进行配置,然后使用 C2 编译器(L4)重新编译代码,进行最大优化。C2 编译器需要有关控制流的大量数据,以便做出优化决策。在本节的后面部分,我们将详细介绍 C2 编译器(JIT)执行的优化。然而,由于流程或优化上下文的变化,优化可能是无效的。在这种情况下,JVM 将取消优化,并将其返回到 L0。
-
B: C2 忙碌:C2 编译是在单独的编译线程上执行的,编译活动被排队。当所有编译线程都忙碌时,JVM 不会遵循正常流程,因为这可能会影响应用程序的整体性能。相反,JVM 将升级到 L2,至少计数器被配置了,稍后,当代码达到更高的阈值时,它将升级到 L3 和 L4。在任何时候,JVM 都可以取消优化或使编译代码无效。
-
C: 简单代码:有时,JVM 会将代码编译到 L3,并意识到代码不需要任何优化,因为它非常直接/简单,基于分析。在这种情况下,它会将其降低到 L1。这样,代码的执行速度会更快。我们越是对代码进行仪器化,对执行就越多的开销。通常观察到 L3 会给执行增加 20-30% 的开销,这是由于仪器化代码造成的。
我们可以使用 -XX:+PrintCompilation 选项来查看 JVM 的行为。以下是一个正常流程的示例:
public class Sample {
public static void main(String[] args) {
Sample samp = new Sample();
while (true) {
for(int i=0; i<1000000; i++) {
samp.performOperation();
}
}
}
public void performOperation() {
int sum = 0;
int x = 100;
performAnotherOperation();
}
public void performAnotherOperation() {
int a = 100;
int b = 200;
for(int i=0; i<1000000; i++) {
int x = a + b;
int y = (24*25) + x;
int z = (24*25) + x;
}
}
}
对于此代码,当我们使用 -XX:+PrintCompilation 执行 java 时,控制台会生成以下日志。日志可以通过使用 +LogCompilation 标志重定向到日志文件:

图 2.6 – 显示分层编译的日志
在这个屏幕截图中,你可以看到 main() 方法是如何从 L0->L3->L4 移动的,这是正常流程(A)。当 JVM 进行优化和去优化,在这些不同的编译级别之间跳跃时,它会达到最优化、最稳定的状态。这是 JIT 编译器相对于 AOT 编译器的最大优势之一。JIT 编译器使用运行时行为来优化代码执行(不仅仅是语义/静态代码优化)。如果你使用 JITWatch 运行它,我们可以看到更清晰的表示。以下屏幕截图显示了当我们通过 Sample.java 碎片运行 JITWatch 工具时的编译链:

图 2.7 – JITWatch 分层编译
上一张屏幕截图显示 Sample::main() 是用 C1-L3 编译器编译的。Sample::Sample()(默认构造函数)被内联,Sample::performOperation() 也被内联到 Sample::main() 中。Sample::performAnotherOperation() 也被编译。这是第一个优化级别:
JITWatch Tiered Compiliation for Sample::main() method
以下屏幕截图显示了各种编译器是如何在每个方法上运行的:

图 2.8 – JITWatch 对 main() 的分层编译
这张屏幕截图显示了 main() 方法的优化。由于 main() 方法有一个长循环,发生了两次 OSR:一次是在 C1 编译代码被替换时,第二次是在 C2 编译代码被替换时。在每种情况下,它都进行了内联。你可以在以下屏幕截图中看到 C1 和 C2 编译器执行了哪些优化:

图 2.9 – JITWatch 对 main() 的分层编译 – OSR-L3
在前一张截图中,我们可以看到 Sample::performAnotherOperation() 被编译,而 Sample::performOperation() 被内联到 Sample::main() 中。下一张截图显示了将 Sample:performAnotherOperation() 内联到 Sample::performOperation() 中所执行的进一步优化。
![Figure 2.10 – JITWatch 分层编译 main() – OSR-L4
![img/B16878_Figure_2.10.jpg]
图 2.10 – JITWatch 分层编译 main() – OSR-L4
现在让我们看看 JIT 编译器是如何优化 Sample::performAnotherOperation() 方法的:
![Figure 2.11 – JITWatch 分层编译 performAnotherOperation()
![img/B16878_Figure_2.11.jpg]
图 2.11 – JITWatch 分层编译 performAnotherOperation()
如前一张截图所示,Sample::performAnotherOperation() 由于运行了长时间的循环,已经经历了各种优化和 OSR。当它达到编译器阈值时,代码被内联到 Sample::performOperation() 中。以下截图揭示了 Sample::performAnotherOperation() 的编译和内联过程。
现在让我们看看 JIT 编译器是如何编译 Sample::performOperation() 方法的:
![Figure 2.12 – JITWatch 分层编译 performOperation()
![img/B16878_Figure_2.12.jpg]
图 2.12 – JITWatch 分层编译 performOperation()
以下截图显示了 performOperation() 方法的 C1 编译链视图:
![Figure 2.13 – JITWatch 分层编译 performOperation() – C1 编译链视图
![img/B16878_Figure_2.13.jpg]
图 2.13 – JITWatch 分层编译 performOperation() – C1 编译链视图
上一张截图显示,当 Sample::performAnotherOperation() 达到编译器阈值时,它被编译,下一张截图显示编译后的代码被内联到 Sample::performOperation() 中:
![Figure 2.14 – JITWatch 分层编译 performOperation() – C2 编译链视图
![img/B16878_Figure_2.14.jpg]
图 2.14 – JITWatch 分层编译 performOperation() – C2 编译链视图
JITWatch 可以用来深入了解 C1 和 C2 编译器的行为以及优化是如何进行的。这有助于反思应用程序代码,并主动更新源代码以获得更好的运行时性能。为了更好地理解 C2 编译器如何优化代码,现在让我们看看 JVM 在编译过程中应用的各类代码优化。
理解 JIT 执行的优化
本节将介绍 JIT 编译器在编译的各个级别上采用的各类优化技术。
内联
对于 JVM 来说,调用方法是一个昂贵的操作。当程序调用方法时,JVM 必须为该方法创建一个新的栈帧,将所有值复制到栈帧中,并执行代码。一旦方法完成,栈帧必须在执行后进行管理。面向对象编程中的一项最佳实践是通过访问方法(获取器和设置器)访问对象成员。
内联是 JVM 执行的最有效的优化之一。JVM 将方法调用替换为代码的实际内容。
如果我们使用以下命令运行之前的代码,我们可以看到 JVM 的代码内联表现:
java -XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining Sample
在这种情况下,对performOperation()方法的调用被替换为内联main()方法的内容。内联有效后,main()方法将看起来像这样:
public static void main(String[] args) {
Sample samp = new Sample();
while (true) {
for(int i=0; i<1000000; i++) {
samp.performOperation();
}
}
可以使用-XX:-Inline标志禁用内联。
JVM 根据方法调用的次数和大小、方法是否频繁调用(热点)以及方法大小是否小于 325 字节来决定是否内联代码。默认情况下,小于 35 字节的方程序会内联。这些数字可以通过命令行的-XX:+MaxFreqInlineSize和-XX:+MaxInlineSize标志进行更改。
单形、双形和多形分派
多态是面向对象编程中的一个关键概念,它提供了一种根据上下文动态加载类的方式,并且行为是动态决定的。接口和继承是多态最广泛使用的实现方式。然而,这伴随着性能开销,因为 JVM 动态加载类/接口实现。内联实现成为一个挑战。
JVM 分析的一个方面是特定实现被调用的次数以及给定基类或接口的实际派生类/接口实现数量。如果分析器只识别出一个实现,那么它被称为单形。如果找到两个,则称为双形,多形意味着存在多个实现。
根据分析,即时编译器会识别出使用了哪个特定的派生类对象(或接口实现),并决定是否内联该特定实现,以克服多态带来的性能开销。单形和多形容易内联。JIT 分析器跟踪执行路径,并识别在哪个上下文中使用了哪个实现,然后执行内联。多形实现复杂,难以内联。以下代码片段展示了多态。我们将使用此代码来理解性能开销:
public interface Shape {
String whichShapeAreYou();
}
public class Circle implements Shape {
public String whichShapeAreYou() { return "I am Circle";}
}
public class Square implements Shape {
public String whichShapeAreYou() { return "I am Square";}
}
public class Triangle implements Shape {
public String whichShapeAreYou() { return "I am Triangle";}
}
public static void main(String[] args) {
//Some code and logic here
switch (circleType) {
case 0:
shape = new Circle();
break;
case 1:
shape = new Square();
break;
case 2:
shape = new Triangle();
break;
default:
System.out.println("Invalid shape");
break;
}
}
在之前的代码中,我们定义了一个名为Shape的接口,并且有三个接口实现,分别是Circle、Square和Triangle。我们使用 switch 语句来初始化正确的类。这里有两种优化场景:
-
如果 JIT 知道使用了特定的实现,它将优化代码并可能进行内联。这被称为单态分发。
-
如果,比如说,决策是基于某个特定的变量或配置,JIT 将进行配置文件分析,这是它可能做出的最乐观的假设,并且只对那些类进行配置文件分析,并内联它们,还可能使用不常见陷阱。如果使用的实现类与假设的不同,JIT 将进行降级优化。
死代码消除
JIT 编译器在配置文件分析过程中识别出从未执行或不需要执行的代码。这被称为死代码,JIT 编译器将其从执行中消除。现代 IDE 识别死代码;这完全是基于静态代码分析。JIT 编译器不仅消除这种琐碎的代码,还根据运行时的控制流消除代码。死代码消除是提高性能的最有效方法之一。
让我们以下面的代码为例:
/**
* DeadCodeElimination
*/
public class DeadCodeElimination {
public void calculateSomething() {
int[] arrayOfValues = new int[1000000];
int finalTotalValue = 0;
for (int i=0; i< arrayOfValues.length; i++) {
finalTotalValue = calculateValue(arrayOfValues[i]);
}
//"Do some more activity here, but never use final Total count");
}
public int calculateValue(int value) {
//use some formula to calucalte the value
return value * value;
}
public static void main(String[] args) {
DeadCodeElimination obj = new DeadCodeElimination();
for (int i=0; i< 10000; i++) {
obj.calculateSomething();
}
}
}
在此代码中,calculateSomething()方法有一些逻辑。让我们看看之前的代码片段。finalTotalValue变量被初始化,后来通过在循环中调用calculateValue()方法来计算总数,但假设finalTotalValue在计算后从未被使用。初始化代码、数组堆分配代码以及调用calculateValue()方法的循环都是死代码。JIT 在运行时理解这一点,并将其完全删除。
JIT 根据配置文件和代码是否可达来做出这些决策。它可能会移除不必要的if语句(特别是空检查;如果对象从未被看到为 null——这种技术有时被称为空检查消除)。它将用所谓的“不常见陷阱”代码来替换这些语句。如果执行过程达到这个陷阱代码,它将进行降级优化。
在通过消除代码放置“不常见陷阱”代码的另一个案例中,是通过预测分支来实现的。基于配置文件分析,JIT 假设并预测了可能永远不会执行的分支代码(if、switch等),并消除了这些代码。
常见子表达式消除是 JIT 用来消除代码的另一种有效技术。在这个技术中,通过移除中间子表达式来减少指令的数量。
在后续的逃逸分析部分,我们还将看到一些基于 JIT 执行的逃逸分析所采用的代码消除技术。
循环优化——循环展开
循环展开是另一种有效的优化技术。这在较小的循环体和大量迭代次数的情况下更为有效。该技术涉及通过替换代码来减少循环的迭代次数。以下是一个非常简单的例子:
for (int i=0; i< arrayOfValues.size; i++) {
somefunction(arrayOfValues[i]);
}
这可以归纳为以下内容:
for (int i=0; i< arrayOfValues.size; i+=4) {
somefunction (arrayOfValues[i]);
somefunction (arrayOfValues[i+1]);
somefunction (arrayOfValues[i+2]);
somefunction (arrayOfValues[i+3]);
}
在这个例子中,即时编译器决定将迭代次数减少 1/4,通过四次调用somefunction()而不是一次。这显著提高了性能,因为跳转语句的数量减少了 1/4。当然,选择四次是基于数组的大小,以确保数组引用不会超出范围。
逃逸分析
逃逸分析是即时编译器执行的最先进的优化之一。这可以通过命令行的-XX:+DoEscapeAnalysis标志来控制。默认情况下是启用的。
在上一章中,我们在内存子系统部分介绍了各种内存区域。堆和栈是两个最重要的内存区域。堆内存区域在 JVM 中的各个线程之间是可访问的。堆不是线程安全的。当多个线程访问存储在堆中的数据时,建议通过获取同步锁来编写线程安全的代码。这将阻止其他线程访问相同的数据。这会影响性能。
栈内存是线程安全的,因为它是为特定的方法调用分配的。只有方法线程可以访问这个区域,因此没有必要担心获取同步锁或阻塞线程的问题。
即时编译器对代码进行详细分析,以识别我们在堆中分配变量,但只在特定的方法线程中使用这些变量的代码,并决定将这些变量分配到“栈区域”而不是“堆区域”。这是即时编译器执行的最复杂的优化之一,对性能有巨大影响。即时编译器可能会决定将变量存储在 PC 寄存器中,以便更快地访问。
即时编译器还会寻找 synchronized和跟踪的使用。如果它被单个线程调用,即时编译器会决定忽略 synchronized。这对性能有重大影响。StringBuffer是线程安全且具有许多同步方法的对象之一。如果StringBuffer的实例不在单个方法之外使用,即时编译器会决定忽略 synchronized。这种技术有时被称为“锁省略”。
在无法忽略同步锁的情况下,即时编译器会尝试合并synchronized块。这种技术被称为锁粗化。这种技术会寻找后续的synchronized块。以下是一个例子:
public class LockCoarsening {
public static void main(String[] args) {
synchronized (Class1.class) {
....
}
synchronized (Class1.class) {
....
}
synchronized (Class2.class) {
....
}
}
}
在这个例子中,两个连续的同步块试图获取同一类的锁。即时编译器会将这两个块合并为一个。
JIT 对在循环中创建且在循环外未使用的变量执行类似的分析。有一个非常复杂的技巧称为“标量替换”,其中 JIT 对创建的对象进行分析,但只使用对象中的一些成员变量。JIT 将决定停止创建对象,并用成员变量直接替换它们。以下是一个非常简单的例子:
class StateStoring {
final int state_variable_1;
final int state_variable_2;
public StateStoring(int val1, int val2) {
this.state_variable_1 = val1;
this.state_variable_2 = val2;
}
}
StateStoring 类是一个简单的类,其中我们使用两个成员 – state_variable_1 和 state_variable_2 来存储对象的状态。JIT 对各种迭代进行分析,并检查该对象是否被创建且从未在作用域外使用。它可能会决定甚至不创建对象,而是用实际的标量(局部变量)替换对象获取器和设置器。这样,就可以避免整个对象创建和销毁(这是一个非常昂贵的进程)。
这里有一个更高级的例子,这次让我们看看 JITWatch 如何显示逃逸分析:
public class EscapeAnalysis2 {
public void createNumberofObjects (int numberOfArraysToCreate, int numberOfCellsInArray) {
for (int i=0; i< numberOfArraysToCreate; i++) {
allocateObjects(numberOfCellsInArray);
}
}
private void allocateObjects(int numberOfCellsInArray) {
int[] arrayObj = new int[numberOfCellsInArray];
for (int i=0; i< numberOfCellsInArray; i++) {
//Heap allocation, which could have been easily a local stack allocation
Integer dummyInt = new Integer(i);
arrayObj[i] = dummyInt.intValue();
}
return;
}
public static void main(String[] args) {
EscapeAnalysis2 obj = new EscapeAnalysis2();
obj.createNumberofObjects(100000, 10);
}
}
在此代码片段中,allocateObjects() 方法正在创建一个数组(在堆上)并将该值添加到数组中。dummyInt 变量的作用域仅限于 allocateObjects() 方法中的 for 循环。没有必要将这些对象创建在堆上。在执行逃逸分析后,JIT 确定这些变量可以放在栈帧中。
下面的 JITWatch 截图展示了这一点:

图 2.15 – JITWatch 逃逸分析 – 1
在此屏幕截图中,分配 dummyInt 的字节码被划掉,以表示该变量的堆分配不是必需的:

图 2.16 – JITWatch 逃逸分析 – 2
之前的屏幕截图显示了 C2/Level 4 执行的优化,其中它移除了变量的分配。
取消优化
在上一节中,我们探讨了 JIT 编译器执行的多种优化技术。JIT 编译器根据分析结果对代码进行优化,并做出一些假设。有时,这些假设在不同的上下文中可能是不正确的。当 JIT 遇到这些场景时,它会取消优化代码,并回退到使用解释器来运行代码。这被称为取消优化,并会影响性能。
有两种情况会发生取消优化:
-
当代码是“不可进入”的
-
当代码是“僵尸”的
让我们通过示例来理解这些场景。
非进入性代码
有两种情况,代码会变成非进入性的:
-
-XX:+PrintCompilation标志。 -
Sample.java:

图 2.17 – 分层编译升级
在前面的屏幕截图中,我们可以看到分层编译的实际操作(第三列显示层级编号)以及所进行的优化。
僵尸代码
在大多数情况下,一些对象被创建在标记为非进入的代码的堆中。一旦 GC 收回所有这些对象,JVM 将将这些类的方法定义为僵尸代码。然后 JVM 从代码缓存中删除这些编译的僵尸代码。正如我们在 深入探讨 hotspot 和 C2 JIT 部分中讨论的那样,保持代码缓存最优非常重要,因为它对性能有重大影响。
正如我们在分层编译中看到的那样,当 Java JIT 在运行时被控制流挑战的任何假设受到挑战时,就会执行去优化。在下一节中,我们将简要介绍 Graal JIT 编译器以及它是如何连接到 JVM 的。
Graal JIT 和 JVM 编译器接口 (JVMCI)
在前面的章节中,当我们遍历 JIT 编译器所经历的各个特性和进步时,很明显 C2 非常复杂。然而,C2 编译器实现有其缺点。C2 是用 C/C++ 语言实现的。虽然 C/C++ 很快,但它不是类型安全的,并且没有垃圾回收。因此,代码变得非常复杂。C2 实现非常复杂,因为它变得越来越难以为新增强和错误修复更改代码。
同时,Java 在许多情况下已经成熟到可以与 C/C++ 一样快速运行。Java 具有类型安全性和垃圾回收。Java 比 C/C++ 更简单,更容易管理。Java 的关键优势是其异常处理能力、内存管理、更好的 IDE/分析工具以及工具支持。JIT 编译器不过是一个接收字节码 byte[],优化它,编译它,并返回机器代码数组 byte[] 的程序。这可以很容易地在 Java 中实现。我们需要的是一个 JVM 接口,它可以提供实现自定义编译器逻辑的协议。这将有助于为新的 JIT 编译器实现打开 JVM。
JDK 增强提案 JEP243 (openjdk.java.net/jeps/243) 是一个提案,旨在提供一个编译器接口,这将使编写 Java 编译器并动态扩展 JVM 以使用它成为可能。
JEP243 在 Java 9 中被添加。这是 JVM 最重大的增强之一。JVMCI 是 JEP243 的一个实现。JVMCI 提供了编写我们自己的 JIT 编译器的所需可扩展性。JVMCI 提供了实现自定义编译器和配置 JVM 以调用这些自定义编译器实现的 API。JVMCI API 提供以下功能:
-
访问 VM 数据结构,这是优化代码所需的
-
管理优化和去优化后的编译代码
-
从 JVM 到执行运行时编译的回调
可以使用以下命令行标志执行 JVMCI:
-XX:+UnlockExperimentalVMOptions
-XX:+EnableJVMCI
-XX:+UseJVMCICompiler
-Djvmci.Compiler=<name of compiler>
Graal 是 JVMCI 的一个实现,它带来了现代 Java 运行时所需的所有关键特性和优化。Graal 完全用 Java 实现。Graal 远不止是一个 JIT 编译器。以下是 Graal JIT 和 JVM JIT (C2) 之间的快速比较:

下一章将更详细地介绍 Graal 架构,以及第四章,Graal 即时编译器,将更深入地探讨 Graal JIT 的工作原理,以及它是如何建立在 Java JIT 之上并引入更多高级优化和多语言支持的。
摘要
在本章中,我们详细介绍了 JIT 编译器的工作原理,并讨论了 JVM 用来优化代码的分层编译模式。我们还通过一些示例代码展示了各种优化技术。这为我们理解 JVM 的内部工作原理提供了良好的理解。
JVMCI 提供了在 JVM 上构建自定义 JIT 编译器的可扩展性。Graal JIT 是 JVMCI 的一个实现。
本章提供了理解 JVM 的工作原理以及 JIT 编译在运行时优化代码的基础。这是理解 Graal JIT 编译器工作原理的关键。
在下一章中,我们将了解 Graal VM 架构是如何建立在 JVM 架构之上,以及它是如何扩展以支持多语言的。
问题
-
什么是代码缓存?
-
可以用来优化代码缓存的各个标志有哪些?
-
什么是编译器阈值?
-
什么是栈上替换?
-
什么是分层编译?分层编译有哪些不同的模式?
-
什么是内联?
-
什么是单态分发?
-
什么是循环展开?
-
什么是逃逸分析?
-
什么是去优化?
-
什么是 JVMCI?
进一步阅读
-
JVM 语言入门(
www.packtpub.com/product/introduction-to-jvm-languages/9781787127944) -
Java SDK 文档(
docs.oracle.com) -
GraalVM 文档(
docs.oracle.com/en/graalvm/enterprise/19/guide/overview/compiler.html) -
JITWatch 文档(
github.com/AdoptOpenJDK/jitwatch)
第四章:第二部分:使用 GraalVM 入门 – 架构与实现
本节将介绍 Graal、GraalVM、Truffle 以及 Graal 的各种架构组件。它还涵盖了 Graal 如何基于 JVM 构建,为多种语言实现提供更优化的虚拟机。此外,本节还包括一个关于如何使用 Graal 的实践环节。本节包含以下章节:
-
第三章, GraalVM 架构
-
第四章, Graal 即时编译器
-
第五章, Graal 即时编译器和原生图像
第五章:GraalVM 架构
在 第一章 Java 虚拟机的发展 中,我们详细探讨了 JVM 架构。在 第二章 JIT、HotSpot 和 GraalJIT 中,我们更详细地介绍了 JVM JIT 编译器的工作原理。我们还探讨了 JVM 如何发展成为最优的 HotSpot VM,拥有 C1 和 C2 JIT 编译器。
虽然 C2 编译器非常复杂,但它已经成为一段非常复杂的代码。GraalVM 提供了一个 Graal 编译器,它基于 C2 编译器的所有最佳实践,但它是完全从零开始用 Java 编写的。因此,Graal JIT 更具有面向对象的特点,拥有现代且易于管理的代码,并支持所有现代集成开发环境、工具和实用程序来监控、调整和管理代码。GraalVM 远不止是 Graal JIT 编译器。GraalVM 引入了一个更大的工具、运行时和 API 生态系统,以支持多种语言(多语言)在 VM 上运行,利用 Graal 提供的最成熟和最坚固的 JIT 编译。
本章,我们将重点关注 GraalVM 架构及其各种组件,以实现最先进、最快的多语言云运行时。我们还将探讨云原生架构模式,以及 GraalVM 是云的最佳平台。
在我们深入了解 GraalVM 架构的细节之前,我们将首先学习现代技术架构的需求。在章节的后面,当我们逐一介绍 GraalVM 架构组件时,我们将解决这些需求。
本章将涵盖以下主题:
-
审查现代架构需求
-
学习 GraalVM 架构是什么
-
审查 GraalVM 版本
-
理解 GraalVM 架构
-
GraalVM 微服务架构概述
-
概述各种可以为 GraalVM 构建代码的微服务框架
-
理解 GraalVM 如何解决各种非功能性方面
到本章结束时,你将对 GraalVM 架构以及各种组件如何协同工作以提供针对多语言应用的全面 VM 运行时有一个非常清晰的理解。
审查现代架构需求
在我们深入探讨 GraalVM 架构之前,让我们首先了解 JVM 的不足之处以及为什么我们需要新的架构和方法。JVM 的旧版本是为传统架构优化的,这些架构是为在数据中心运行的长运行应用程序而构建的,提供高吞吐量和稳定性(例如,单体 Web 应用程序服务器和大型客户端应用程序)。一些微服务是长运行的,Graal JIT 也将提供最佳解决方案。随着我们转向云原生,整个架构范式已经转变为组件化、模块化、分布式和异步架构,以高效运行并满足高可扩展性和可用性要求。
让我们将这些内容分解为现代云原生架构的更具体要求。
更小的占用空间
应用程序由细粒度的模块化组件(微服务)组成,以实现高可扩展性。因此,构建具有较小占用空间的应用程序非常重要,这样它们就不会消耗过多的 RAM 和 CPU。随着我们转向云原生部署,这甚至更为重要,因为我们在云上实现的是按使用付费。占用空间越小,我们可以在云上使用更少的资源运行得越多。这对总拥有成本(TCO),作为关键业务 KPI 之一,有直接影响。
更小的占用空间也有助于我们快速连续地做出更改并部署它们。这在敏捷世界中非常重要,因为系统是构建来拥抱变化的。随着商业的快速变化,应用程序也需要快速拥抱变化以支持商业决策。在传统的单体架构中,即使是微小的更改也需要整体构建、测试和部署。在现代架构中,我们需要灵活性,以便以模块化的方式推出功能更改,而不会使生产系统崩溃。
我们有新的工程实践,例如 A/B 测试,在这种测试中,我们与旧版本并行测试这些功能模块(微服务),以决定新版本是否足够好,可以发布。我们执行金丝雀部署(滚动更新),其中应用程序组件得到更新,而不会停止生产系统。我们将在本章后面的DevOps – 持续集成和交付部分更详细地介绍这些架构要求。
更快的启动
可扩展性是最重要的要求之一。现代应用程序是构建来根据负载快速扩展和缩减的。负载呈指数增长,现代应用程序需要优雅地处理任何负载。随着更小的占用空间,也期望这些应用程序组件(微服务)能够快速启动以开始处理负载。随着我们向更多无服务器架构迈进,应用程序组件预计将根据请求处理启动和关闭。这需要一个非常快速的启动策略。
更快的引导和更小的占用空间也带来了挑战,即使用可嵌入的虚拟机构建应用程序组件。基于容器的方案要求这些应用程序组件是不可变的。
多语言和互操作性
多语言是现实:每种语言都有其自身的优势,并将继续存在,因此我们需要接受这一事实。如果你看看解释器/编译器的核心逻辑,它们都是相同的。它们都试图达到类似的优化水平,并生成具有最小占用空间的运行最快的机器代码。我们需要的是一个最佳平台,可以运行用不同语言编写的各种应用程序,并允许它们之间进行互操作性。
在考虑了这些架构要求列表之后,现在让我们了解 GraalVM 的工作原理以及它是如何满足这些要求的。
学习 GraalVM 架构是什么
GraalVM 提供了一个 Graal JIT 编译器,这是 JVMCI 的实现(我们在上一章中已经介绍过),它完全基于 Java,并以 C2 编译器优化技术为基础,在此基础上构建。Graal JIT 比一个 C2 编译器要复杂得多。GraalVM 是 JDK 的替代品,这意味着所有目前在 JDK 上运行的应用程序都应该在 GraalVM 上运行,而无需对应用程序代码进行任何更改。
虽然 GraalVM 是基于 Java 构建的,但它不仅支持 Java,还支持使用 JavaScript、Python、R、Ruby、C 和 C++ 进行多语言开发。它提供了一个可扩展的框架,称为 Truffle,允许任何语言在平台上构建和运行。
GraalVM 还提供了 AOT 编译功能,以静态链接的方式构建原生镜像。GraalVM 包含以下运行时、库和工具/实用程序列表(这是针对 GraalVM 20.3.0 版本的。最新组件列表可以在 www.graalvm.org/docs/introduction/ 找到。)
首先,让我们看一下以下表格中的核心组件:

接下来,让我们看一下以下表格中列出的额外工具和实用程序:

既然我们已经了解了 GraalVM 的组件,我们将探讨 GraalVM 可用的各种版本,以及这些版本之间的差异。
检查 GraalVM 版本(社区和企业)
GraalVM 可作为社区版和企业版提供:
-
社区版:GraalVM 社区版(CE)是一个开源版本,作为 OpenJDK 发行版构建。GraalVM 的大多数组件是 GPL 2,带有类路径例外许可。有关许可的更多详细信息,请参阅
github.com/oracle/graal#license。GraalVM CE 基于 OpenJDK 1.8.272 和 OpenJDK 11.0.9。GraalVM CE 由社区支持。它可以部署在生产环境中。然而,它不包含 Oracle 提供的要求支持服务。Oracle 还提供可下载的 Docker 镜像,用于测试和评估(有关更多详细信息,请参阅www.graalvm.org/docs/getting-started/container-images/)。 -
企业版:GraalVM 企业版(EE)是在 GraalVM OTN 许可协议下的授权版本。此版本可用于评估和构建非生产应用程序。GraalVM EE 提供了额外的性能(比 CE 快约 20%),更小的占用空间(比 CE 小约 2 倍),安全性(原生代码内存保护),以及运行生产级企业应用程序的可伸缩性。EE 版包含额外的调试工具,如 Ideal Graph Visualizer,它不仅有助于调试性能问题,还有助于在 GraalVM 上优化应用程序以获得最佳性能。GraalVM EE 还提供支持服务。对于 Oracle 云客户,GraalVM EE 支持作为订阅的一部分提供。GraalVM EE 还有一个托管模式,它更好地管理堆,避免页面错误和崩溃。GraalVM EE 适用于已订阅 Java SE 的客户。
现在我们已经了解了 GraalVM 的各种可用版本以及它所包含的运行时、工具和框架,让我们深入了解 GraalVM 的架构。
理解 GraalVM 架构
在本节中,我们将探讨 GraalVM 的各种架构组件。我们将了解各种运行时、工具和框架是如何结合在一起以提供最先进的虚拟机和运行时的。以下图表显示了 GraalVM 的高级架构:
![图 3.1 – Graal VM 架构]
![img/B16878_Figure_3.1.jpg]
图 3.1 – Graal VM 架构
让我们详细地逐一介绍这些组件。
JVM(HotSpot)
JVM HotSpot 是常规的 Java HotSpot VM。HotSpot VM 中的 C2 编译器被 Graal JIT 编译器实现所取代。Graal JIT 编译器是 Java 虚拟机编译器接口(JVMCI)的一个实现,并插入到 Java VM 中。我们在前面的章节中介绍了 JVM HotSpot 的架构。请参考它们以深入了解 JVM HotSpot 的工作原理以及 JVM 的各种架构组件。
Java 虚拟机编译器接口(JVMCI)
JVMCI 在 Java 9 中引入。这允许编写作为插件使用的编译器,JVM 可以调用以进行动态编译。它提供了一个 API 和协议,用于构建具有自定义实现和优化的编译器。
在这个上下文中,“编译器”一词指的是即时编译器。我们在前几章中详细介绍了 JIT 编译器。GraalVM 使用 JVMCI 来访问 JVM 对象,与 JVM 交互,并将机器代码安装到代码缓存中。
Graal JIT 实现有两种模式:
-
libgraal:libgraal是一个 AOT 编译的二进制文件,由 HotSpot VM 作为本地二进制文件加载。这是默认模式,也是推荐使用 HotSpot VM 运行 GraalVM 的方式。在此模式下,libgraal使用自己的内存空间,不使用 HotSpot 堆。这种模式的 Graal JIT 具有快速的启动时间和改进的性能。 -
jargraal:在这种模式下,Graal JIT 被像任何其他 Java 类一样加载,因此它将经历一个预热阶段,并使用解释器运行,直到识别出热方法并进行优化。可以通过从命令行传递--XX:-UseJVMCINativeLibrary标志来调用此模式。
在 OpenJDK 9+、10+ 和 11+ 中,我们使用 -XX:+UnlockExperimentalVMOptions、-XX:+UseJVMCICompiler 和 XX:+EnableJVMCI 标志来运行 Graal 编译器,而不是 C2 编译器。默认情况下,GraalVM 使用 Graal JIT 编译器。始终建议使用 GraalVM 发行版,因为这些版本包含最新的更改。OpenJDK 以较慢的速度合并更改。
在下一章中,我们将通过示例代码详细说明 Graal JIT 如何比 C2 JIT 更好,我们将使用 Graal 伴随的调试工具和实用程序来演示 Graal JIT 在运行时执行的优化。
Graal 编译器和工具
Graal 编译器基于 JVMCI 构建,提供了一个更好的 JIT 编译器(C2,我们在前几章中讨论过)实现,并进行了进一步优化。Graal 编译器还提供了一个 AOT(Graal AOT)编译选项,可以构建可以独立运行并嵌入虚拟机的原生镜像。
Graal JIT 编译器
我们在第一章,“Java 虚拟机的发展”中探讨了 JVM 架构。为了参考,以下是 JVM 的高级架构概述:

图 3.2 – 带有 C2 编译器的 JVM 架构
如您所见,C1 和 C2 编译器将 JIT 编译作为 JVM 执行引擎的一部分实现。我们详细介绍了 C1 和 C2 如何根据编译阈值优化/去优化代码。
GraalVM 替换了 JVM 中的 JIT 编译器,并进一步优化。以下图表显示了 GraalVM 的高级架构:

图 3.3 – 带有 Graal 编译器的 VM 架构
JVM JIT 编译器和 Graal JIT 之间的一个区别是,Graal JIT 是构建来优化中间代码表示(抽象语法树(AST)和 Graal 图,或 Graal 中间表示)。Java 在编译时将代码表示为 AST 和中间表示。
任何语言的表达式和指令都可以被转换并表示为抽象语法树(AST);这有助于从优化代码的逻辑中抽象出特定语言的语法和语义。这种方法使得 GraalVM 能够优化和运行任何语言的代码,只要代码可以被转换为 AST。我们将在第四章“Graal Just-In-Time 编译器”中深入探讨 Graal 图和 AST。
Graal JIT 编译器的四个关键组件如下:
-
分析器:正如其名所示,它分析运行中的代码,并生成代码优化器用于做出决策或对优化做出假设的信息。
-
中间代码生成器:这个生成中间代码表示,它是代码优化器的输入。
-
代码优化器:这个使用由配置文件收集的数据来优化中间代码。
-
目标代码生成器:然后,优化后的代码被转换为目标机器代码。
以下图表展示了 Graal JIT 在非常高级的层面上是如何工作的:

图 3.4 – Graal JIT 编译 – 高级流程图
让我们更好地理解这个流程图:
-
JVM 语言(Java、Kotlin、Groovy 等)的代码在 Graal JIT 上以原生方式运行,并且 Graal JIT 优化代码。
-
非 JVM 语言(如 JavaScript 和 Ruby)使用 Truffle API 实现语言解析器和解释器。语言解释器将代码转换为 AST 表示形式。Graal 在中间表示形式上运行 JIT 编译。这有助于利用 Graal 实现的所有高级优化技术来优化非 JVM 语言。
-
原生基于 LLVM 的语言(如 C/C++、Swift 和 Objective C)遵循一条略微不同的路径来转换为中间表示。Graal Sulong 用于创建 Graal 所使用的中间表示。我们将在本章后面讨论 Truffle 和 Sulong。
Graal JIT 优化策略
Graal JIT 优化策略是从头开始构建的,基于 C2 JIT 编译器优化策略的最佳实践。Graal JIT 在 C2 优化策略之上构建,并提供了更高级的优化策略。以下是 Graal JIT 编译器执行的一些优化策略:
-
部分逃逸分析
-
改进的内联(
aleksandar-prokopec.com/resources/docs/prio-inliner-final.pdf) -
连接式 lambda
-
交叉过程优化
我们将在下一章中,通过示例代码和示例,详细介绍这些优化策略。
Truffle
Truffle 框架是一个用于构建解释器和工具/实用程序(如集成开发环境、调试器和性能分析器)的开源库。Truffle API 用于构建可以在 GraalVM 上运行的、利用 GraalVM 提供的优化功能的语言解释器。
Graal 和 Truffle 框架由以下 API 组成,这些 API 使多语言(Polyglot)成为可能:
-
语言实现框架:此框架由语言实现者使用。它还附带了一个名为 SimpleLanguage 的语言参考实现。我们将在 第九章,Graal Polyglot – LLVM, Ruby, and WASM 中详细介绍。
-
Polyglot API:这组 API 帮助不同语言(客语言)编写的代码与 Java(宿主语言)之间进行交互。例如,一个 Java(宿主)程序可以嵌入 R(客)语言代码以执行一些机器学习/AI 逻辑。Polyglot API 提供了框架,将帮助语言程序员管理客语言和宿主语言之间的对象。
-
仪器化:Truffle 仪器化 API 为工具/工具构建者提供了构建集成开发/调试环境、工具和实用程序的框架。使用 Truffle 仪器化 API 构建的工具和实用程序可以与任何使用 Truffle 实现的语言一起工作。这为各种语言提供了一致的开发者体验,并利用了 JVM 的复杂调试/诊断功能。
图 3.5 展示了 Truffle 作为 GraalVM 和其他语言解释器之间中间层的高级架构。各个语言解释器使用 Truffle API 实现。Truffle 还提供了一个互操作性 API,用于在跨各种语言实现的方法之间调用方法和传递数据:

图 3.5 – Truffle 架构
如前图所示,Java 应用程序直接在 GraalVM 上运行,由 Graal 编译器取代 C2 JIT 编译器。其他语言程序在 Truffle 语言实现框架之上运行。相应的语言解释器使用 Truffle 实现解释器。Truffle 将代码与解释器结合,使用部分评估生成机器代码。
AST 是中间表示形式。AST 提供了表示语言语法结构的最佳方式,其中通常,父节点是操作符,子节点表示操作数或操作符(基于基数)。以下图表展示了 AST 的粗略表示:

图 3.6 – 简单表达式的 AST
在此图表中,a、b和c可以是任何变量(对于弱类型语言)。解释器开始基于各种执行的配置文件假设“泛型”。然后它开始假设具体细节,并使用部分评估来优化代码。
Truffle(使用 Truffle 编写的语言解释器)作为解释器运行,而 Graal JIT 开始识别代码中的优化。
优化基于推测,最终,如果在运行时证明推测是错误的,JIT 将重新优化和重新编译(如前图所示)。重新优化和重新编译是一项昂贵的任务。
部分评估从代码和数据中创建语言的中间表示形式,随着它学习并识别新的数据类型,它将去优化到 AST 解释器,应用优化,并进行节点重写和重新编译。在某个点上,它将具有最优表示。以下图表解释了 Truffle 和 Graal 如何优化中间表示形式:

图 3.7 – Graal JIT 的 AST 优化
让我们更好地理解这个图表:
-
表达式被简化为 AST。在 AST 节点中,叶节点是操作数。在此示例中,我们选取了一个非常简单的表达式来理解部分评估是如何工作的。在 JavaScript 等非强类型语言中,a、b 和 c 可以是任何数据类型(有时被称为泛型)。在表达式中评估泛型是一个昂贵的操作。
-
根据配置文件,Graal JIT 推测并假设一个特定的数据类型(在此示例中,为整数),优化代码以评估整数表达式,并编译代码。
-
在此示例中,它使用内联优化策略。Graal JIT 编译器根据用例应用了各种其他优化策略。
-
当在运行时,编译器识别到一个控制流,其中一个操作数实际上不是整数时,它将去优化,并使用新的数据类型重新编写 AST,并优化代码。
-
经过几次运行此优化/去优化迭代后,编译器最终将生成最优化代码。
这里的关键区别是 Graal 正在处理 AST 并生成优化代码,只要代码表示为 AST,它就不关心源代码是用什么语言编写的。
下面的图示展示了不同语言在 GraalVM 上运行的高级流程,其中 Truffle 作为中间层,以在 GraalVM 上执行任何编程语言代码:

图 3.8 – Truffle 和 Graal 编译流程图
此图展示了 Truffle 作为非 JVM 语言和 GraalVM 之间层的简化表示。代码也可以直接与 Substrate VM 构建为原生镜像。
Truffle API 与自定义注解处理器一起使用,以生成解释器源代码,然后进行编译。Java 代码不需要中间表示形式。它可以直接编译在 GraalVM 上运行。我们将在第九章,“Graal 多语言 – LLVM、Ruby 和 WASM”中讨论 Truffle 解释器和如何编写自定义解释器。我们将在第六章,“Truffle – 概述”中介绍 Truffle 多语言 API。
Truffle 还提供了一个名为Truffle Instrument API的框架,用于构建工具。仪器提供细粒度的虚拟机级运行时事件,可用于构建分析、跟踪、分析和调试工具。最好的部分是,使用 Truffle 构建的语言解释器可以使用 Truffle 仪器的生态系统(例如,VisualVM、Chrome 调试器和 GraalVM Visual Studio Code 扩展)。
Truffle 提供了多语言互操作性协议。此协议定义了每种语言需要实现的协议,并支持在多语言应用程序之间传递数据。
Sulong – LLVM
LLVM 是一个开源项目,它是一组模块化、可重用的编译器和工具链。有很多语言(C、C++、Fortran、Rust、Swift 等)编译器是基于 LLVM 构建的,其中 LLVM 提供中间表示形式(也称为 LLVM-IR)。
Sulong 管道与其他在 Truffle 上运行的编程语言编译器所看到的有所不同。以下图示展示了 C/C++代码的编译过程:

图 3.9 – LLVM 编译流程图
此图展示了用 C 编写的应用程序代码如何在 GraalVM 上编译和运行。在 C/C++等本地语言中编写的应用程序代码在 Clang 中编译成中间表示形式。此 LLVM 中间表示形式在基于 Truffle API 构建的 LLVM 中间表示形式解释器上运行。Graal JIT 将在运行时进一步优化代码。
SubstrateVM(Graal AOT 和原生镜像)
Graal 上的应用可以在 GraalVM 或 SubstrateVM 上部署。SubstrateVM 是一种可嵌入的虚拟机代码,在 AOT 编译过程中被打包成原生镜像。
Graal AOT 编译是创建特定目标操作系统/架构的原生二进制文件的一种非常强大的方式。对于云原生工作负载和无服务器架构,这是一个非常强大的选项,可以实现更小的体积、更快的启动速度,更重要的是,可嵌入的运行时(提供不可变性)。
快速组件化模块化部署(容器)也带来了管理和版本控制挑战。这通常被称为配置漂移,这是我们面对高可用性环境中大量容器时遇到的主要问题之一。通常,容器基础设施由一个团队构建,随着时间的推移,它由不同的团队管理。总有这样的情况,我们被迫在某个环境中更改 VM/容器/OS 的配置,而我们可能永远无法追踪。这导致了生产环境和 DR/HA 环境之间的差距。
不可变基础设施(镜像)帮助我们更好地进行基础设施版本控制。它还增加了我们在测试中的信心,因为我们应用程序容器运行的基础设施是不可变的,我们对测试结果有信心。要构建不可变组件,我们需要一个具有小体积的嵌入式虚拟机(VM)。SubstrateVM 提供了这个嵌入式 VM。
在 AOT 编译中,代码直接编译成机器代码并执行。没有运行时分析或优化/去优化。Graal AOT 编译器(也称为“原生镜像”编译器)对代码执行静态分析和静态初始化,并生成嵌入 VM 的可执行代码。AOT 执行的优化基于代码的可达性。以下图显示了编译过程:

图 3.10 – Graal AOT 编译
此图展示了 Graal AOT 如何编译原生镜像并将 SubstrateVM 作为原生镜像的一部分嵌入。AOT 编译的一个缺点是 VM 不能像 JIT 那样根据运行时分析优化代码。为了解决这个问题,我们使用基于配置引导的优化策略来捕获应用程序的运行时指标,并使用这些配置数据通过重新编译来优化原生镜像。
配置引导优化(PGO)
GraalVM 使用配置引导优化(PGO)根据运行时分析数据优化原生镜像。这是仅在企业版中可用的功能之一。以下图显示了 PGO 管道的工作方式:

图 3.11 – 使用 PGO 的 Graal AOT 编译
让我们更好地理解这个工作流程:
-
当使用
native-image编译代码时,我们使用--pgo-instrumented标志。这将告诉编译器向编译后的代码中注入检测代码。 -
当我们开始运行这个经过仪表化的原生镜像时,分析器开始收集运行时数据,然后开始创建配置文件(
.ipof)。 -
一旦我们用各种工作负载(所有可能的工作负载 – 以捕获尽可能多的仪表数据)运行了原生镜像,我们就可以使用
--pgo标志(native-image --pgo=profile.iprof)重新编译原生镜像,提供配置文件作为输入。Graal 原生镜像编译器创建最佳原生镜像。
我们将在下一章通过真实示例构建带有配置文件引导优化的原生镜像,并了解原生镜像中的内存管理是如何工作的。
GraalVM 是现代微服务架构的优秀运行时。在下一节中,我们将介绍 GraalVM 的各种功能,这些功能有助于构建微服务应用程序。
GraalVM 微服务架构概述
GraalVM 是微服务架构的理想选择。对于某些微服务架构来说,最重要的要求之一是更小的占用空间和更快的启动速度。GraalVM 是在云中运行多语言工作负载的理想运行时。市场上已经有一些原生框架,可以构建在 GraalVM 上运行最佳的应用程序,例如 Quarkus、Micronut、Helidon 和 Spring。这些框架在作为原生镜像运行时被发现几乎快了 50 倍。我们将在 第十章 “使用 GraalVM 的微服务架构”中详细介绍 GraalVM 是微服务的正确运行时和平台。
理解 GraalVM 如何解决各种非功能性方面
在本节中,我们将介绍微服务云原生架构的典型非功能性要求,以及 GraalVM 如何解决这些要求。
性能和可伸缩性
性能和可伸缩性是微服务云原生架构中更重要的非功能性要求之一。微服务由 Kubernetes 等编排器自动扩展和缩减。这要求微服务建立在启动速度快且运行快速的运行时上,消耗最少的云资源。GraalVM AOT 编译有助于构建与 C/C++ 等原生语言相当性能的原生镜像。
要了解 AOT 编译的代码(原生镜像)为什么比 JIT 编译的代码更快,让我们看看 JIT 和 AOT 在运行时遵循的步骤:


图 3.12 – Graal JIT 与 AOT 流程图
此图展示了 JIT 和 AOT 的高级步骤。JIT 通过在运行时分析代码来优化代码一段时间。由于 JVM 在运行时执行了额外的分析、优化和去优化操作,因此存在性能开销。
根据 Apache Bench 基准测试,观察到虽然 GraalVM JIT 的吞吐量和性能在开始时低于 AOT,但随着请求数量的增加,大约在每秒 14,000 个请求后,Graal JIT 进行了优化,并比 Graal AOT 表现更好。
观察到 Graal AOT 的性能比 Graal JIT 快 50 倍,并且内存占用比 Graal JIT 小 5 倍。
Graal AOT 与 PGO 的吞吐量一致,有时甚至优于 Graal JIT。然而,对于长时间运行的任务,Graal JIT 可能具有更好的吞吐量。因此,为了获得最佳吞吐量和一致的性能,使用带有 PGO 的 Graal AOT 是最佳选择。
请参阅在www.infoq.com/presentations/graalvm-performance/和www.graalvm.org/why-graalvm/发布的基准研究。
与学术合作伙伴一起发布了更多的基准研究,请参阅renaissance.dev。
我们可以得出以下结论:
-
GraalVM 本地图像(AOT)对于快速启动和需要更小内存占用的应用最佳,例如无服务器应用和容器微服务。
-
GraalVM JIT 在峰值吞吐量方面表现最佳。吞吐量对于长时间运行的过程来说非常重要,在这些过程中,可扩展性是关键。这可能是高流量的 Web 应用服务器,例如电子商务服务器和股市应用。
-
通过结合垃圾收集配置和 JIT,可以帮助降低延迟。延迟对于应用的响应性非常重要。当我们运行高吞吐量时,有时垃圾收集会减慢响应速度。
使用它并没有一条固定的规则。这取决于我们需要在 JIT 和 AOT 之间做出的各种组合决策,以及可能的各种其他配置。我们将在下一章探讨各种编译器和本地图像配置。
安全性
GraalVM 的安全性建立在 JVM 安全性的基础上,后者基于沙盒模型。让我们快速回顾一下沙盒模型是如何工作的:


图 3.13 – JVM 安全模型
在 Java 2 安全架构中,所有类文件都由字节码验证器验证(请参阅前几章以获取有关类加载器的更多详细信息)。字节码验证器检查类文件是否有效,并寻找任何溢出、下溢、数据类型转换、方法调用、对类的引用等问题。
一旦字节码被验证,依赖的类将由类加载器加载。请参阅第一章,Java 虚拟机的发展,以了解类加载器子系统的工作原理。类加载器与安全管理者及访问控制一起工作,强制执行在策略文件中定义的安全规则。通过网络下载的 Java 代码将检查签名(表示为java.security.CodeSource,包括公钥)。
安全管理者(java.lang.SecurityManager)是处理授权最重要的组件。安全管理者有各种检查以确保授权完成。访问控制器(java.security.AccessController)类是另一个关键类,它有助于控制对系统资源的访问。
密钥库是一个密码保护的存储库,其中包含所有私钥和证书。存储库中的每个条目也可以是密码保护的。
Java 安全是可扩展的,有自定义安全实现称为安全提供者。
GraalVM 建立在 Java 安全模型之上,并将其抽象化以在中间表示级别强制执行安全。GraalVM 不建议在安全管理者上运行不受信任的代码。
GraalVM 安全模型使用 Truffle 语言实现框架 API 为 JVM 主机应用程序创建执行上下文,并将其传递给访客应用程序(用不同语言编写的应用程序代码)。以下图显示了 GraalVM 如何允许访客和主机应用程序交互操作以及如何控制访问的高级架构:

图 3.14 – Graal 安全模型
执行上下文(org.graalvm.polyglot.Context)定义了访客应用程序的访问控制。基于在执行上下文中定义的访问控制,访客应用程序可以访问系统的资源。GraalVM 提供了一个 Polyglot API 来创建这些访问控制,通过执行上下文来设置访问各种函数的权限,例如文件 I/O、线程和原生访问。根据主机设置的权限,访客将拥有相应的访问权限。使用看门狗线程来限制上下文的时间。看门狗将在指定的时间内关闭上下文,以释放资源,并基于时间限制访问。
以下代码演示了如何设置执行上下文:
Context context = Context.newBuilder().allowIO(true).build();
Context context = Context.newBuilder() .fileSystem(FileSystem fs).build();
Context context = Context.newBuilder() .allowCreateThread(true).build()
Context context = Context.newBuilder() .allowNativeAccess(true).build()
GraalVM 还提供了一个 API,用于在主机和访客应用程序之间交换对象:
-
@HostAccess.Export注解,例如)。 -
主机到访客数据交换:从主机传递给访客的对象需要由访客语言处理。数据通过上下文传递,例如:
Value a = Context.create().eval("js", "21 + 21");
主机应用程序可以将值a返回给 JavaScript 访客应用程序,其值为42(在评估后)。
我们将在第六章“Truffle – 概述”中详细讲解,通过一个真实示例来帮助理解。
GraalVM EE 还为 LLVM 中间表示代码提供了一种管理执行模式,以处理任何内存违规和故障。请参阅docs.oracle.com/en/graalvm/enterprise/19/guide/security/security-guide.html获取更多详细信息。
DevOps – 持续集成和交付
DevOps 自动化是任何现代云原生架构的核心要求之一。GraalVM 与 DevOps 管道集成得非常好。以下图表展示了典型的 GitOps 管道及其代表性软件(GraalVM 集成到 DevOps 软件栈的任何堆栈中):

图 3.15 – 使用 GraalVM 的 GitOps
让我们更好地理解这个图表。
持续集成(CI)管道由 Git 仓库中应用程序代码和基础设施代码更改的典型拉取请求触发。可以使用 GitHub actions、Argo CD 或 CicleCI 等 CI 工具来编排 CI 管道。典型的 CI 管道包括以下步骤:
-
构建:在构建阶段,从 Git 仓库的适当分支拉取标记为发布的代码。代码经过验证(任何静态代码分析)并构建。对于云原生,通常使用 Graal AOT 编译器将代码构建为原生镜像。
-
测试:使用单元测试脚本来测试代码,并进一步验证是否存在任何安全漏洞。
-
打包:一旦代码通过所有测试,通常将代码打包到云原生目标运行时(使用 Docker 镜像、VM 或任何其他二进制格式)。目标可以是无服务器容器、Docker 容器或 VM。
-
存储:最终二进制存储在二进制存储或仓库中,如 Docker Hub 或 Red Hat Quay(如果是 Docker 镜像)。
持续部署管道可以基于发布计划触发,或者可以手动触发(取决于发布计划和策略)。持续部署通常具有以下阶段:
-
部署验证:将最终二进制 ID 部署到可以现在进行端到端测试的环境。可以遵循以下各种策略:
a. 传统上:我们有集成测试环境和用户验收测试环境(或预生产环境)用于不同级别的验证和测试。
b. 蓝/绿部署:存在两个并行环境(称为蓝和绿)。其中一个将在生产环境中运行,让我们假设是蓝环境。绿环境可以用来测试和验证我们的代码。一旦我们确认新版本运行良好,我们使用路由器切换到绿环境,并使用蓝环境来测试未来的版本。这提供了一种高可用性的应用部署方式。
c. 金丝雀部署和滚动更新:金丝雀部署是一种更近期的使用相同环境进行生产和验证的方法。这是一个测试我们的代码并与当前版本(A/B 测试)比较的出色功能。金丝雀部署提供了一个 API 管理层,可以根据各种参数(例如测试用户或来自特定部门的用户可以访问新版本,而最终用户仍在使用旧版本)将流量重定向到特定的端点。应用可以部署在特定数量的服务器/节点上(通过百分比或数量)。随着我们对新版本越来越有信心,我们可以通过增加运行新版本的节点数量来进行滚动更新,并向更广泛的用户群体开放。这也提供了按阶段推出新版本(按地区或用户人口统计或任何参数)的灵活性。
-
测试:执行了各种级别的测试,包括功能性和非功能性测试。大部分测试都是通过自动化完成的,而且这也是由持续交付管道编排的。
-
生产部署:一旦所有测试都完成,最终应用将被部署到生产环境。再次强调,这种部署可能使用传统的、蓝/绿或金丝雀策略之一。
GraalVM 提供了一种非常灵活的方式来部署应用,无论是作为独立应用、容器、云、虚拟机还是 Oracle 数据库。存在非常复杂的微服务框架,如 Quarkus、Micronaut 和 Fn 项目,它们为 GraalVM 提供原生支持,并与现代 GitOps 工具很好地集成。
摘要
在本章中,我们探讨了 GraalVM 架构。Graal JIT 是 JIT 编译器的新实现,它取代了 C2 编译器,并带来了更多的优化。Graal JIT 完全用 Java 实现。Truffle 提供了解释器实现框架和多语言框架,以便将其他非 JVM 语言引入 GraalVM。
本章提供了对 GraalVM 随附的各种运行时、框架、工具、Graal 更新器和实用工具的良好理解。我们还探讨了 GraalVM 的两个可用版本以及这两个版本之间的关键区别。我们详细介绍了 GraalVM 架构的所有各种组件。我们还探索了架构的一些非功能性方面,包括安全模型、性能和 DevOps。如果你想要了解如何使用 GraalVM 来构建跨各种语言的云原生微服务和高性能应用程序,这一点非常重要。
在下一章中,我们将深入探讨 Graal JIT 的工作原理,如何使用 Graal 提供的各种工具来理解 Graal JIT 的内部工作方式,以及如何使用这些工具来调试和微调我们的代码。
问题
-
GraalVM 有哪些版本?
-
什么是 JVMCI?
-
什么是 Graal JIT?
-
什么是 Graal AOT?PGO 如何帮助 AOT 编译?
-
什么是 Truffle?它是如何帮助在 GraalVM 上运行多种语言代码的?
-
什么是 SubstrateVM?
-
什么是 Guest Access Context?
-
为什么 GraalVM 是云原生微服务的理想运行时?
进一步阅读
-
轻量级云原生 Java 应用程序 (
medium.com/graalvm/lightweight-cloud-native-java-applications-35d56bc45673) -
Java on Truffle — 实现完全元循环 (
medium.com/graalvm/java-on-truffle-going-fully-metacircular-215531e3f840) -
GraalVM (
www.graalvm.org/) -
GraalVM 企业版 (
docs.oracle.com/en/graalvm/enterprise/20/index.html) -
GraalVM Git (
github.com/oracle/graal)
第六章: Graal 即时编译器
在第三章“GraalVM 架构”中,我们探讨了 GraalVM 架构及其构成的各种组件。我们详细介绍了带有 Truffle 的 GraalVM 多语言架构,并简要提到了 Graal 的即时(JIT)编译器。我们探讨了 Graal JIT 如何通过实现 Java 虚拟机编译器接口插入 Java 虚拟机。在本章中,我们将通过运行示例代码并使用理想图可视化器工具可视化 Graal JIT 执行的 Graal 图和优化来探索 Graal JIT 编译器的工作原理。
在本章中,我们将涵盖以下主题:
-
设置环境
-
理解 Graal JIT 编译器
-
理解 Graal 编译器优化
-
调试和监控应用程序
到本章结束时,你将对 Graal JIT 编译器的工作原理有一个非常清晰的理解,了解各种优化技术,知道如何使用理想图可视化器诊断和调试性能问题,并且能够微调 Graal JIT 编译器配置以获得最佳性能。
技术要求
在本章中,我们将使用一些示例代码并使用工具进行分析。以下是一些遵循本章所需的工具/运行时:
-
OpenJDK (
openjdk.java.net/) -
GraalVM (
www.graalvm.org/) -
VisualVM (
visualvm.github.io/index.html) -
理想图可视化器
-
以下是一些示例代码片段,这些代码片段可在我们的 Git 仓库中找到。代码可以从
github.com/PacktPublishing/Supercharge-Your-Applications-with-GraalVM/tree/main/Chapter04下载。 -
本章的“代码在行动”视频可在
bit.ly/3fmPsaP.找到。
设置环境
在本章中,我们将使用 VisualVM 和理想图可视化器来理解 Graal JIT 的工作原理。这种理解将有助于我们在后续章节中用 Graal 构建最佳代码。
设置 Graal
在第三章“GraalVM 架构”中,我们讨论了 Graal 的两个版本——社区版和企业版(EE)。社区版可以从技术要求部分提到的 Git 仓库中下载,而 EE 则需要你注册 Oracle 以下载。EE 可用于免费评估和非生产应用。
安装社区版
要安装 GraalVM 社区版,请访问 github.com/graalvm/graalvm-ce-builds/releases,并下载针对目标操作系统(macOS、Linux 和 Windows)的最新版本。在撰写本书时,最新版本是 21.0.0.2,基于 Java 8 或 Java 11 版本。社区版基于 OpenJDK 构建。
请按照下一部分提供的针对目标操作系统的说明进行操作。最新说明可以在 https://www.graalvm.org/docs/getting-started/#install-graalvm 找到。
在 macOS 上安装 GraalVM
对于 macOS,下载 GraalVM 归档文件后,解压归档并将解压文件夹的内容复制到 /Library/Java/JavaVirtualMachines/<graalvm>/Contents/Home。
一旦我们复制了文件,我们必须导出路径以访问 GraalVM 二进制文件。让我们在终端上运行以下 export 命令:
export PATH=/Library/Java/JavaVirtualMachines/<graalvm>/Contents/Home/bin:$PATH
export JAVA_HOME=/Library/Java/JavaVirtualMachines/<graalvm>/Contents/Home
对于 macOS Catalina 及更高版本,需要移除 quarantine 属性。可以使用以下命令完成:
sudo xattr -r -d com.apple.quarantine <graalvm-path>
如果没有这样做,你会看到以下错误信息:
![图 4.1 – 在 MacOS 上运行 Graal 时出现的错误信息
![img/Figure_4.1_B16878.jpg]
图 4.1 – 在 MacOS 上运行 Graal 时出现的错误信息
SDKMAN 提供了一种自动安装 GraalVM 的方法。有关更多详细信息,请参阅 sdkman.io/。
在 Linux 上安装 GraalVM
要在 Linux 上安装 GraalVM,请解压下载的 zip 文件,将其复制到任何目标文件夹,并将 PATH 和 JAVA_HOME 路径设置为指向提取文件的文件夹。为此,请在命令行上执行以下命令:
export PATH=<graalvm>/bin:$PATH
export JAVA_HOME=<graalvm>
在 Windows 上安装 GraalVM
要在 Windows 上安装 GraalVM,请解压 .zip 文件,将其复制到任何目标文件夹,并将 PATH 和 JAVA_HOME 路径设置为指向提取文件的文件夹。要设置 PATH 环境变量,请在终端上执行以下命令:
setx /M PATH "C:\Progra~1\Java\<graalvm>\bin;%PATH%"
setx /M JAVA_HOME "C:\Progra~1\Java\<graalvm>"
要检查安装和设置是否完成,请在终端上运行 java -version 命令。
执行命令后,你应该会看到以下类似输出(我在使用 GraalVM EE 21.0.0 和 Java 11。你应该看到你安装的版本):
java version "11.0.10" 2021-01-19 LTS
Java(TM) SE Runtime Environment GraalVM EE 21.0.0 (build 11.0.10+8-LTS-jvmci-21.0-b06)
Java HotSpot(TM) 64-Bit Server VM GraalVM EE 21.0.0 (build 11.0.10+8-LTS-jvmci-21.0-b06, mixed mode, sharing)
现在我们来探索 GraalVM 安装的文件夹结构。在 GraalVM 安装文件夹中,你可以找到以下表格中解释的文件夹结构:

在上一章中,我们详细介绍了 Graal 伴随的各种运行时、工具和实用程序。Graal 更新器是用于安装可选运行时的非常重要的工具之一。要检查可用的运行时,请执行 gu list。以下截图显示了典型的输出:
![图 4.2 – Graal 更新器列表
![img/Figure_4.2_B16878.jpg]
图 4.2 – Graal 更新器列表
我们可以运行 gu install <runtime> 来安装其他运行时。
安装 EE
GraalVM EE 可免费用于试用和非生产用途。可以从 www.graalvm.org/downloads/ 下载。
选择所需的 GraalVM Enterprise 版本。网站将重定向您到 Oracle 的注册页面。如果您已经注册,您应该能够登录,然后将被重定向到一个可以下载 GraalVM 和支持工具的页面。在撰写本书时,屏幕看起来大致如下截图:

图 4.3 – GraalVM EE 下载页面
您可以选择要下载的 EE 版本以及基础 JDK 版本。在撰写本书时,Java 8 和 Java 11 是两个可行的版本。当您滚动此页面时,您将找到以下下载链接:
-
Oracle GraalVM Enterprise Edition 核心:这是 GraalVM 的代码。
-
Oracle GraalVM Enterprise Edition 本地图像工具:这是一个本地图像工具。它也可以稍后使用 Graal 更新器下载。
-
理想图可视化器:这是一个非常强大的 Graal 图分析工具。您需要为此章节下载它。请参阅“安装理想图可视化器”部分的说明。
-
GraalVM LLVM 工具链:如果您想在 GraalVM 上编译和运行 C/C++ 应用程序,则需要此 LLVM 工具链。
-
Oracle GraalVM Enterprise Edition Ruby 语言插件:这是一个 Ruby 语言编译器和运行时。它也可以稍后使用 Graal 更新器下载。
-
Oracle GraalVM Enterprise Edition Python 语言插件:这是一个 Python 语言编译器和运行时。它也可以稍后使用 Graal 更新器下载。
-
Oracle GraalVM Enterprise Edition WebAssembly 语言插件:这是一个 WebAssembly 语言编译器和运行时。它也可以稍后使用 Graal 更新器下载。
-
Oracle GraalVM Enterprise Edition 在 Truffle 上的 Java:这是在 Truffle 解释器上的 JVM 实现。
在版本之间切换
我们可以在同一台机器上安装多个版本的 GraalVM,并且可以在这些不同的发行版之间切换。在本章中,我们将切换到不同的发行版以比较它们的性能。在发行版之间切换的最佳方式是使用 Visual Studio Code。Visual Studio Code 提供了一个 GraalVM 插件,帮助我们添加各种发行版,并且只需单击一下按钮,就可以在各个发行版之间切换。请参阅 www.graalvm.org/tools/vscode/ 和 marketplace.visualstudio.com/items?itemName=oracle-labs-graalvm.graalvm 获取更多详细信息。请参阅本章后面的 调试和监控应用程序 部分,以获取有关如何安装 Visual Studio Code 并用于调试应用程序的更多详细信息。
我们还可以创建 shell 脚本来在各个发行版之间切换,通过设置 PATH 和 JAVA_HOME 环境变量指向适当的发行版。
安装 Graal VisualVM
Java VisualVM 是分析应用程序堆、线程和 CPU 利用率的最强大工具之一。VisualVM 广泛用于分析核心转储、堆转储和离线的应用程序。它是一个非常复杂的工具,可以识别瓶颈并优化 Java 代码。
自 JDK 9 以来,VisualVM 已迁移并升级为 Graal VisualVM。Graal VisualVM 扩展了功能,包括对 Graal 进程的分析,并目前支持 JavaScript、Python、Ruby 和 R。Graal VisualVM 还为原生图像进程提供了一些有限的监控和分析功能。Graal VisualVM 与 Graal Community Edition 和 EE 一起捆绑提供。Graal VisualVM 可在 .bin/jvisualvm(Windows 的 .exe)中找到。
让我们快速浏览一下 Graal VisualVM 的关键特性。Graal VisualVM 具有一个非常直观的界面。主窗口的左侧面板(见 图 4.3)显示了所有 本地 和 远程 进程。使用这个功能,我们可以轻松连接到这些进程以开始我们的分析:

图 4.4 – VisualVM,左侧面板
一旦我们连接到进程,在右侧面板中,我们将看到以下五个选项卡:
- 概述:在此选项卡中,我们可以看到进程配置、JVM 参数和系统属性。以下截图显示了我们所运行的 FibonacciCalculator 进程的典型屏幕:

图 4.5 – VisualVM – 应用概述
- 监控:在此选项卡上,我们可以看到 CPU 使用率、堆分配、加载的类数量、正在运行的线程数量等。我们还可以强制进行垃圾收集以查看进程的行为。我们可以执行堆转储以对堆分配进行更深入的分析。以下是窗口的截图:

图 4.6 – VisualVM – 应用程序监控
- 线程:此选项卡提供了有关正在运行进程的各种线程的详细信息。我们还可以捕获线程转储以进行进一步分析。此选项卡不仅显示了活动线程,我们还可以分析已完成的线程。以下是一个典型的线程选项卡截图:

图 4.7 – VisualVM – 应用程序线程
这里是一个典型的线程转储截图,可以用来确定是否存在任何死锁或线程等待:

图 4.8 – VisualVM – 线程转储
- 采样器:此选项卡可以用来对运行中的进程进行快照,并对 CPU、内存等进行分析。以下是点击快照按钮所获取的快照的内存使用截图:

图 4.9 – VisualVM – 使用快照的内存使用
- 分析器:这就像采样器,但它一直运行。除了 CPU 和内存之外,我们还可以查看 JDBC 调用和获取响应所需的时间。下一张截图显示了 CPU 分析:

图 4.10 – VisualVM – 应用程序分析器
除了这个之外,Graal VisualVM 还可以用来分析核心转储,以确定任何 Java 进程崩溃的根本原因。在撰写本书时,Graal VisualVM 支持 JavaScript 和 Ruby(仅限于堆、对象视图和线程视图),Python 和 R(仅限于堆和对象视图)。
JDK 飞行记录器(JFR)分析是 VisualVM 的另一个强大功能。它帮助我们分析通过 JFR 连接的数据,而不会对运行中的进程产生开销。JFR 提供了更高级的分析,包括捕获和分析文件 I/O、套接字 I/O 和线程锁,除了 CPU 和线程之外。
Graal VisualVM 还提供了扩展 API,因此我们可以编写自定义插件。可以使用各种插件来扩展 Graal VisualVM。以下是一些最广泛使用的插件:
-
可视 GC 插件:此插件提供了一个强大的界面来监控垃圾收集、类加载器和 JIT 编译器的性能。这是一个非常强大的插件,可以识别代码中的优化以改进性能。
-
跟踪器:跟踪器提供了一个更好的用户界面,用于详细监控和分析应用程序。
-
启动分析器:正如其名所示,它提供了用于分析启动过程并识别可以执行的优化以改进启动的仪器。
你可以在visualvm.github.io/pluginscenters.html找到可用的插件完整列表。
安装理想图可视化器
Ideal Graph Visualizer is a very powerful tool for analyzing how Graal JIT is performing various optimizations. This requires an advanced understanding of Graal Graphs, which is an intermediate representation. Later in this chapter, we will cover Graal Graph and how to use the Ideal Graph Visualizer so that we can see how Graal performs various optimizations. This is critical, as it helps us write better code and optimize the code at development time, and reduces the load on the compiler to perform it just in time.
The Ideal Graph Visualizer is available with GraalVM EE. It can be downloaded from the Oracle website. The Ideal Graph Visualizer can be launched with the following command, after setting the PATH to the location where it has been unzipped/installed:
idealgraphvisualizer
The --jdkhome flag can be used to point to the right version of GraalVM. Once it has been launched, you will see the following screen:
![Figure 4.11 – 理想图可视化器 – 主窗口
![img/Figure_4.11_B16878.jpg]
图 4.11 – 理想图可视化器 – 主窗口
The Ideal Graph Visualizer requires Graal dumps to render and analyze Graal Graphs. Graal dumps can be created using the following command:
java -Dgraal.Dump=:n <java class file>
在前面的命令中,n可以是1、2或3,每个数字代表一个详细程度级别。这将生成一个名为graal_dumps的文件夹,其中包含bgv文件(二进制图文件)。有时你会因为无效化和重新编译(去优化或栈上替换)而找到各种bgv文件(请参阅第二章,JIT、HotSpot 和 GraalJIT,并查找栈上替换部分以获取更多信息)。这些bgv文件可以在理想图可视化器中打开以进行分析。一旦bgv文件被加载,你将看到如下屏幕:
![Figure 4.12 – 理想图可视化器 – 主窗口–Graal 转储
![img/Figure_4.12_B16878.jpg]
图 4.12 – 理想图可视化器 – 主窗口–Graal 转储
The left pane can be used to navigate through the various phases of compilation and optimization, the main window shows the graph, and the right pane can be used to configure how to render these graphs. We can view the Graal Graphs, Call Graph, AST, and Truffle Call Tree.
The Ideal Graph Visualizer can also be connected from the Java runtime (using the Dgraal.PrintGraph=Network flag) to view the graphs in real time, while the application code is executing.
In the next section, we will explore how these Graal Graphs can be read to understand how the Graal compiler works.
理解 Graal JIT 编译器
在上一章中,我们简要介绍了 Graal 编译器和其生态系统。在本节中,我们将深入了解各种编译器选项,并了解 Graal 如何即时优化代码。在下一节中,我们将探讨即时编译,以及如何创建原生镜像。在我们深入了解 Graal 编译器的工作原理之前,让我们快速浏览一些可以传递给虚拟机的 Graal 编译器配置。
Graal 编译器配置
Graal 编译器可以通过传递给 java 命令(在 GraalVM 版本的 java 中)的各种参数进行配置。在本节中,我们将介绍一些最有用的命令行配置。
我们将在一个示例应用程序上尝试这些不同的标志,以查看它如何影响 Graal 编译器。
让我们编写一个简单的 Java 类,称为 FibonacciCalculator。以下是类的源代码:
class FibonacciCalculator{
public int[] findFibonacci(int count) {
int fib1 = 0;
int fib2 = 1;
int currentFib, index;
int [] fibNumbersArray = new int[count];
for(index=2; index < count; ++index ) {
currentFib = fib1 + fib2;
fib1 = fib2;
fib2 = currentFib;
fibNumbersArray[index - 1] = currentFib;
}
return fibNumbersArray;
}
public static void main(String args[])
{
FibonacciCalculator fibCal = new FibonacciCalculator();
long startTime = System.currentTimeMillis();
long now = 0;
long last = startTime;
for (int i = 1000000000; i < 1000000010; i++) {
int[] fibs = fibCal.findFibonacci(i);
long total = 0;
for (int j=0; j<fibs.length; j++) {
total += fibs[j];
}
now = System.currentTimeMillis();
System.out.printf("%d (%d ms)%n", i , now – last);
last = now;
}
long endTime = System.currentTimeMillis();
System.out.printf ("total: (%d ms)%n", System.currentTimeMillis() - startTime);
}
}
如您所见,我们正在生成 1000000000 到 1000000010 个斐波那契数,然后稍后计算生成的所有斐波那契数的总和。代码被编写成循环以触发编译阈值。
JIT 有很多优化机会。让我们首先使用 Java HotSpot 运行此程序:

图 4.13 – FibonacciCalculator – Java HotSpot 输出
如您所见,初始迭代花费了最多时间,并且经过迭代优化到大约 1,300 毫秒。现在让我们使用从 Graal EE 分发中获得的 javac 编译代码,并使用 Graal JIT 运行相同的程序。以下截图显示了使用 GraalVM(Java 11 上的 GraalVM EE 21.0.0.2)运行相同应用程序的输出:

图 4.14 – FibonacciCalculator – GraalVM 输出
我们可以看到性能有显著提升。Graal 的起始速度与 Java HotSpot 相似,但在迭代过程中优化到 852 毫秒,而使用 HotSpot 运行则需要 1,300 毫秒。以下选项用于禁用 GraalJIT 并在 GraalVM 上使用 HotSpot:
-XX:-UseJVMCICompiler
这通常用于比较 Graal 的性能。让我们使用 GraalVM EE 21.0.0.2 编译器运行此选项,并使用前面的源代码:
java -XX:-UseJVMCICompiler FibonacciCalculator/
以下是在运行前面命令后的输出截图:

图 4.15 – FibonacciCalculator – GraalVM (21/Java 11) 输出
如您所见,尽管我们使用的是 Graal 编译器,但性能与 Java HotSpot 相似,实际上比 Java HotSpot 15 慢。请注意,我们的 Graal 是在 Java 11 上运行的。
CompilerConfiguration 标志用于指定要使用哪个 JIT 编译器。以下是我们可以传递给设置编译器配置的参数:
-Dgraal.CompilerConfiguration
我们有三个选项;让我们也用我们的示例代码运行这些选项,看看它的性能如何:
-Dgraal.CompilerConfiguration=enterprise:这使用企业 JIT,并生成最优代码。然而,由于编译,会有初始的减速:

图 4.16 – Fibonacci 计算器 – 企业编译器配置
-Dgraal.CompilerConfiguration=community:这会产生社区版本的 JIT,它优化到相当的程度。因此,编译速度更快。

图 4.17 – Fibonacci 计算器 – 社区编译器配置
-Dgraal.CompilerConfiguration=economy:这编译得很快,优化较少:

图 4.18 – Fibonacci 计算器 – 经济型编译器配置
我们可以看到在使用企业、社区和经济配置时性能的显著差异。以下是三种选项性能的比较:

图 4.19 – Fibonacci 计算器 – 企业、社区和经济配置对比
除了这个之外,还有很多其他性能调整选项可以用来提高编译器的性能,例如这个:
-Dgraal.UsePriorityInlining (true/false)
前面的标志可用于启用/禁用高级内联算法。禁用此选项可以提高编译时间并帮助提高吞吐量。
此标志可用于禁用自动向量化优化:
-Dgraal.Vectorization (true/false)
此标志可用于禁用路径重复优化,例如基于支配的重复模拟。当禁用时,它会对吞吐量产生影响:
-Dgraal.OptDuplication (true/false)
This next flag can be set to values between -1 and 1\. When the value is below 0, the JIT reduces the effort spent on inlining. This will improve the startup and provides throughput. When the value is greater than 0, the JIT spends more effort in inlining, increasing the performance:
-Dgraal.TuneInlinerExploration (-1 to +1)
这是一个非常有用的标志,可以启用以跟踪 JIT 编译器在内联优化上的决策:
-Dgraal.TraceInlining (true/false)
当我们为示例代码启用此标志时,我们得到以下结果:
compilation of FibonacciCalculator.main(String[]):
at FibonacciCalculator.main(FibonacciCalculator.java:20) [bci: 4]: <GraphBuilderPhase> FibonacciCalculator.<init>(): yes, inline method
at FibonacciCalculator.main(FibonacciCalculator.java:25) [bci: 32]: <GraphBuilderPhase> FibonacciCalculator.findFibonacci(int): no, bytecode parser did not replace invoke
compilation of FibonacciCalculator.main(String[]):
at FibonacciCalculator.main(FibonacciCalculator.java:20) [bci: 4]: <GraphBuilderPhase> FibonacciCalculator.<init>(): yes, inline method
at FibonacciCalculator.main(FibonacciCalculator.java:25) [bci: 32]:
├──<GraphBuilderPhase> FibonacciCalculator.findFibonacci(int): no, bytecode parser did not replace invoke
└──<PriorityInliningPhase> FibonacciCalculator.findFibonacci(int): yes, worth inlining according to the cost-benefit analysis.
compilation of java.lang.String.hashCode():
at java.lang.String.hashCode(String.java:1504) [bci: 19]:
├──<GraphBuilderPhase> java.lang.String.isLatin1(): no, bytecode parser did not replace invoke
└──<PriorityInliningPhase> java.lang.String.isLatin1(): yes, budget was large enough to inline this callsite.
at java.lang.String.hashCode(String.java:1504) [bci: 29]:
├──<GraphBuilderPhase> java.lang.StringLatin1.hashCode(byte[]): no, bytecode parser did not replace invoke
└──<PriorityInliningPhase> java.lang.StringLatin1.hashCode(byte[]): yes, budget was large enough to inline this callsite.
我们可以看到 JIT 编译器是如何在内联上做出决策的。
这些优化标志甚至可以设置在其他 GraalVM 启动器上,例如 js(用于 JavaScript)、node 和 lli。
Graal JIT 编译器管道和分层优化
在上一章中,在 Graal JIT 编译器 部分,我们探讨了 Graal JIT 如何通过 JVMCI 与虚拟机集成。在本节中,让我们更深入地了解 Graal JIT 如何与虚拟机交互。
Graal 在三个级别上优化代码。分层方法有助于 Graal 从更平台无关的表示(高级中间表示)开始执行优化,到更平台相关的表示(低级中间表示)。以下图表显示了 Graal JIT 如何与虚拟机接口并执行这三个级别的优化:

图 4.20 – Graal JIT 编译器 – 编译层级
让我们尝试更好地理解这张图:
-
当虚拟机达到编译阈值时,它将字节码和元数据传递给 Graal JIT(参考 第二章,JIT、HotSpot 和 GraalJIT,了解更多关于编译阈值的信息)。
-
Graal 解析字节码并生成 高级中间表示(HIR)。
-
然后,它对 HIR 执行各种优化。这些是一些标准 Java 优化技术,以及 Graal 中引入的一些新技术,例如部分逃逸分析和高级内联技术。
-
一旦执行了这些高级优化,Graal 开始将高级操作转换为低级操作。这个阶段称为降低。在这个阶段,它执行两个层级的优化,并最终为目标处理器架构生成 低级中间表示(LIR)。
-
一旦对 LIR 执行所有优化,就会生成最终的优化机器代码,并将其存储在代码缓存中,同时还包括垃圾收集器将使用的引用映射和用于反优化的元数据。
在本节中,我们探讨了 Graal JIT 编译器内部的工作原理,并研究了各种会影响编译器性能的编译器配置。现在让我们更好地理解 Graal 中间表示。
Graal 中间表示
AddNode、IfNode 和 SwitchNode,它们都继承自基类 Node。边(操作数)表示为类的字段。以下图显示了各种类型节点的层次结构:

图 4.21 – Graal 图节点 – 类层次结构
在 SSA 中表示代码可以创建每个值的单个变量版本。这有助于执行更好的数据流分析和优化。使用 phi 函数(Φ)将基于决策的控制路径(如 if 和 switch)转换为单一代码。Phi 函数是两个值的函数,值的选择基于控制流。有关 SSA 的更多详细信息,请参阅以下论文:https://gcc.gnu.org/onlinedocs/gccint/SSA.html 和 en.wikipedia.org/wiki/Static_single_assignment_form。关键点是,整个程序被转换为 SSA 以执行优化。
Graal IRs 作为 Graal 图构建,其中每个节点都有指向创建操作数的节点的输入边,以及显示控制流的后续边。后续边指向在控制流方面继当前节点之后的节点。
为了演示我们迄今为止讨论的所有内容,让我们使用理想图可视化器分析一些简单的 Java 代码。这段代码中的逻辑可能不会生成一个简单的图——代码故意保持简单。循环是为了达到阈值,当 JVM 达到阈值时,它将执行 Graal JIT 编译,如下所示:
public class DemonstrateGraalGraph {
public long calculateResult() {
long result = 0;
for (int i=0; i<2000; i++) {
result = result + i;
}
return result;
}
public static void main(String[] args) {
DemonstrateGraalGraph obj = new DemonstrateGraalGraph();
while (true) {
//This loop is just to reach the compiler threshold
long result = obj.calculateResult();
System.out.println("Total: " + result);
}
}
}
现在我们使用 javac DemonstrateGraalGraph.java 命令来编译前面的代码。为了保持图简单,我们将只编译 calculateResult() 方法,使用 -XX:CompileOnly=DemonstrateGraalGraph:calculateResult 标志。我们还将使用以下标志禁用一些优化:
-Dgraal.FullUnroll=false, -Dgraal.PartialUnroll=false, -Dgraal.LoopPeeling=false, -Dgraal.LoopUnswitch=false, -Dgraal.OptScheduleOutOfLoops=false, 和 -Dgraal.VectorizeLoops=false
因此,我们得到以下内容:
java -XX:CompileOnly=DemonstrateGraalGraph::calculateResult \
-XX:-UseOnStackReplacement \
-Dgraal.Dump=:1 \
-XX:+PrintCompilation \
-Dgraal.FullUnroll=false \
-Dgraal.PartialUnroll=false \
-Dgraal.LoopPeeling=false \
-Dgraal.LoopUnswitch=false \
-Dgraal.OptScheduleOutOfLoops=false \
-Dgraal.VectorizeLoops=false \
DemonstrateGraalGraph
这将创建一个名为 graal_dumps 的文件夹,其中包含所有 Graal JIT 活动的转储。一旦你加载由 Graal 生成的 bgv 文件,你将在左侧面板中找到列出的各种优化层,如下面的截图所示:

图 4.22 – 理想图可视化器 – DemonstrateGraalGraph – 左侧面板
当你点击 calculateResult() 方法时,正如我们要求 JVM 只编译 calculateResult() 方法一样。让我们更好地理解这个图:

图 4.23 – 理想图可视化器 – DemonstrateGraalGraph – 解析后的 Graal 图
程序从 0 Start 开始,循环从 7 LoopBegin 节点开始。为了使图更容易理解,一些部分被标签 A 和 B 突出显示。让我们探索这些图的部分是什么。
部分 A
-
部分 A 突出了
for循环。它被转换成了18 if语句。if语句的输入是 I 的当前值,它是 Phi 节点9 Phi(4,22,i32)和常数 2000 节点11 C(2000) i32的输出。 -
Phi 节点连接在控制流合并的地方。在这种情况下,9 Phi (4,22, i32) 合并了
4 C(0) i32(for循环中的i=0)和22 +节点的输出(即i++)。这个节点将简单地输出i在增加 21 C(1) i32 节点 值后的当前值。 -
然后,这个表达式流入 12 < 节点,并与 11 C(2000) i32(循环的最大值)进行比较,这个表达式由控制流节点 18 if 进行评估。
部分 B
-
部分 B 突出了计算结果的部分。
-
结果的初始值表示为
i64,因为我们将其声明为long。 -
result = result + i表达式。i的值从I流出,I从 9 Phi(4,22, i32) 流出。 -
当循环在 18 if 处结束时,最终输出流向 24 Return。
现在我们可以通过在左侧面板中选择优化阶段来遍历每个优化阶段,以查看代码是如何被优化的。让我们快速看一下这个图是如何通过各个阶段进行转换的。当我们选择左侧面板的 大纲 窗口中的 Before Phase Lowering 时,我们将看到以下图:

图 4.24 – 理想图可视化器 – 展示 Graal 图 – Graal 图在降低之前
在这个阶段,我们可以看到以下优化:
-
19 Sign Extend 节点被替换为 27 Zero Extend,因为编译器发现它是一个无符号整数。与有符号整数相比,无符号整数的操作成本更低。
-
12 < 节点被替换为 26 |<|,这是一个无符号小于操作,速度更快。编译器基于各种迭代和性能分析得出这个结论。由于操作数被认为是无符号的,因此操作也认为是无符号的。
-
该图还说明了应用规范化技术,用 < 替换 <=,以加快
if(最初是for循环)语句。
后续阶段 – 高级、中级和低级 – 可能不会显示显著的优化,因为代码相对简单,并且我们已经禁用了一些优化以保持图的可读性和易于理解:

图 4.25 – 理想图可视化器 – 展示 Graal 图 – Graal 图,其他层级
图 4.26 是所有优化都启用时的图示。您将看到循环展开被非常突出地使用来加速循环:

图 4.26 – 理想图可视化器 – 展示 Graal 图 – 最终优化的图
Graal 作为分层编译的一部分执行各种优化。我们将在下一节中详细介绍这一点,并了解我们如何利用这些知识来改进我们的代码编写方式。
理解 Graal 编译器优化
Graal 编译器在即时对代码执行一些最先进的优化。以下小节将讨论其中最关键的。
在进入本节之前,请参考第二章中“理解 JIT 执行的优化”部分,JIT、HotSpot 和 GraalJIT。
投机优化
JIT 编译依赖于代码的运行时分析。正如我们所见,图是基于热点进行优化的。热点,如我们在第二章中“JIT、HotSpot 和 GraalJIT”所讨论的,是程序最频繁经过的控制流。试图优化整个代码是没有意义的;相反,JIT 编译器试图优化热点控制路径/流。这是基于投机和假设。当执行过程中假设被证明是错误的时候,编译器会迅速去优化,并等待基于新的热点进行优化的另一个机会。我们在第二章中“JIT、HotSpot 和 GraalJIT”的“编译器阈值”部分讨论了编译器的阈值和热点。Graal JIT 也使用类似的技术来识别热点。Graal 执行我们在第二章中“JIT、Hotspot 和 GraalJIT”的“理解 JIT 执行的优化”部分所讨论的所有优化,但还使用了一些高级技术。让我们来看看 Graal JIT 应用于代码的一些最重要的优化技术。
部分逃逸分析
在第二章中“理解 JIT 执行的优化”部分,我们探讨了逃逸分析。逃逸分析是最强大的技术之一。它确定了对象的作用域以及对象从局部作用域逃逸到全局作用域。如果它识别出不会逃逸的对象,就有机会进行优化,编译器将优化代码以使用栈分配而不是堆分配来处理局部作用域内的对象。这可以在堆内存的分配和释放上节省大量时间。
部分逃逸分析进一步扩展了这一概念,不仅限于识别逃逸方法级别作用域的控制分支。当发现对象仅在特定控制流中逃逸时,这有助于优化代码。其他对象未逃逸的控制流可以被优化为使用局部值或标量替换。
部分逃逸分析寻找可能通过方法调用、返回值、抛出语句等方式发生的逃逸。让我们用一个简单的代码示例来理解它是如何工作的:
public void method(boolean flag) {
Class1 object1 = new Class1();
Class2 object2 = new Class2();
//some processing
object1.parameter = value;
//some more logic
if(flag) {
return object1;
}
return object2;
}
上述代码是一些示例代码,仅用于说明部分逃逸分析。在这段代码中,我们创建 object1 作为 Class1 的实例,object2 作为 Class2 的实例。正在进行一些处理,并使用计算出的某些值更新 object1 的字段。根据标志,object1 或 object2 将逃逸。假设大多数时间标志是 false,只有 object1 逃逸,因此每次方法调用时创建 object1 没有意义。这段代码被优化为以下类似的内容(这只是一个说明部分逃逸分析如何工作的示例;Graal JIT 可能不会进行这种精确的重构):
public void method(boolean flag) {
Class2 object2 = new Class2();
tempValue = value;
if(flag) {
Class1 object1 = new Class1();
object1.parameter = tempValue;
return object1;
}
return object2;
}
object1 仅在需要时创建,并使用临时变量存储中间值,如果 object1 需要初始化,则使用它逃逸之前的临时值。这优化了堆分配时间和堆大小。
跨过程分析和内联
Graal 在 AST/Graph 层级进行优化。这有助于 Graal 进行跨过程分析和识别任何可能永远不会为空的选项,并跳过编译代码的这一部分,因为它可能永远不会被调用。它为该代码块添加了一个守卫,以防万一。如果控制流通过该块,JIT 可以降级代码。
要理解跨过程分析和内联,一个常用的例子是 JDK 类 OptionalDouble。以下是 OptionalDouble 类的一个片段:
public class OptionalDouble {
public double getAsDouble() {
if (!isPresent) {
throw new NoSuchElementException("No valuepresent");
}
return value;
}
}
假设我们调用这个 getAsDouble() 方法,并且方法有一个 throw 块,但那个 throw 块可能永远不会被调用。Graal 编译器将编译所有代码,除了 if 块,并将放置一个 guard 语句,以便如果它被调用,它可以降级代码。除此之外,Graal 还执行更高级的内联来优化代码。我们可以通过传递 -Dgraal.Dump=:2 来查看 Graal 执行的完整优化集。在 Graal 溢出级别 2 时,我们得到每个阶段的更详细的图列表。在下一张屏幕截图中,你可以看到 Graal JIT 在编译的各个层级上对代码执行的整个优化列表:

图 4.27 – 理想图可视化器 – 展示 GraalGraph – 编译层级
通过查看图在每一步的优化情况,我们可以看到代码在开发时间可以优化的每个区域。这将减少 Graal JIT 的负载,代码将表现得更好。其中一些优化技术已在 第二章 的 理解 JIT 执行的优化 部分中介绍,JIT、HotSpot 和 GraalJIT。
调试和监控应用程序
GraalVM 随带了一套丰富的工具,用于调试和监控应用程序。我们已经探讨了 VisualVM 和理想图可视化器。正如你在前面的章节中看到的,这两个工具非常适合详细分析。这种分析还提供了关于我们如何在开发时间改进代码以减少 Graal JIT 的负载,并编写高性能和低内存占用 Java 代码的见解。除了这两个工具之外,以下是一些 Graal 随带的其他工具。
Visual Studio Code 扩展
Visual Studio Code 扩展是 Graal 最强大的集成开发环境之一。以下截图显示了 Visual Studio Code 的 GraalVM 扩展:

图 4.28 – Visual Studio Code 上的 GraalVM 环境
在上一张截图的左侧面板中,你可以看到所有已配置的 GraalVM 安装。在各个版本的 GraalVM 之间切换非常容易,终端和整个环境都将使用所选的 GraalVM。
此扩展还使得安装可选组件变得容易。我们不必手动运行 gu 命令。此扩展提供了一种简单的方式来构建、调试和运行用 Java、Python、R、Ruby 和 Polyglot(混合语言代码)编写的代码。
此扩展可以直接从 Visual Studio Code 扩展选项卡中通过搜索 Graal 进行安装。以下截图显示了扩展安装页面:

图 4.29 – 安装 Visual Studio Code 的 GraalVM 扩展
还有一个包含额外功能的 GraalVM 扩展包,例如 Micronaut 框架集成和 NetBeans 语言服务器,它们提供 Java 代码补全、重构、Javadoc 集成以及许多其他高级功能。下一张截图显示了 GraalVM 扩展包的安装页面:

图 4.30 – Visual Studio Code 的 GraalVM 扩展包插件
你可以在 GraalVM 网站上了解更多关于此扩展的信息,网址为 www.graalvm.org/tools/vscode/graalvm-extension/。
GraalVM 仪表板
GraalVM 仪表板是一个基于 Web 的工具,我们可以用它来执行静态和动态编译的详细分析。这对于原生图像分析非常有用。该工具提供了关于编译、可达性、可用性、性能数据、动态编译参数、去优化等方面的详细仪表板报告。
我们将在下一章中运行此工具,当我们创建示例代码的原生图像并执行原生图像代码的更详细分析时。
命令行工具
在 Polyglot 的上下文中,有两个命令行工具可以用来识别进一步优化代码的机遇。我们将在第六章“Truffle – 概述”中,用于 Polyglot 优化时使用这些工具。以下是与 GraalVM 一起提供的两个命令行工具:
-
性能分析 CLI:这个工具有助于识别优化 CPU 和内存使用的机遇。请参阅
www.graalvm.org/tools/profiling/获取更多详细信息。 -
代码覆盖率 CLI:这个工具记录并分析每次执行的代码覆盖率。这对于运行测试用例和确保良好的代码覆盖率非常有用。此工具还可以识别可以消除的可能死代码,或在开发时间可以优化的热点代码。请参阅
www.graalvm.org/tools/code-coverage/获取更多详细信息。
Chrome 调试器
Chrome 调试器提供了 Chrome 开发者工具扩展,用于调试客户端语言应用程序。当使用--inspect选项运行应用程序时,可以使用 Chrome 调试器。可以从developers.google.com/web/tools/chrome-devtools/安装此扩展。我们将在第六章“Truffle – 概述”中讨论 JavaScript 和 Node.js 在 Graal 上的使用时介绍此工具。
摘要
在本章中,我们详细介绍了 Graal JIT 和即时编译器。我们通过一个示例代码,查看 Graal JIT 如何使用理想图可视化器执行各种优化。我们还详细介绍了 Graal 图。这是非常关键的知识,将帮助你分析并识别在开发过程中可以应用以加快运行时 Graal JIT 编译的优化。
在本章中,你已经详细了解了 Graal JIT 编译的内部工作原理以及如何微调 Graal JIT。你还对如何使用一些高级分析和诊断工具来调试 Graal JIT 编译以及识别优化代码的机会有了很好的理解。
在下一章中,我们将更详细地探讨 Graal 的即时编译。
问题
-
Graal JIT 编译的各个层级是什么?
-
什么是中间表示?
-
SSA 是什么?
-
什么是投机优化?
-
逃逸分析与部分逃逸分析有什么区别?
进一步阅读
-
Java 的局部逃逸分析和标量替换(
ssw.jku.at/Research/Papers/Stadler14/Stadler2014-CGO-PEA.pdf) -
理解基本 Graal 图 (
chrisseaton.com/truffleruby/basic-graal-graphs/) -
GraalVM 优化策略 (
www.beyondjava.net/graalvm-plugin-replacement-to-jvm) -
GraalVM 企业版 (EE) (
docs.oracle.com/en/graalvm/enterprise/19/index.html) -
GraalVM 文档 (
www.graalvm.org/docs/introduction/)
第七章:Graal 提前编译器和原生图像
Graal 的提前编译有助于构建启动速度更快、占用空间更小的原生图像,比传统的 Java 应用程序更小。原生图像对于现代云原生部署至关重要。GraalVM 附带了一个名为native-image的工具,用于提前编译并生成原生图像。
native-image将代码编译成一个可以在没有虚拟机的情况下独立运行的可执行文件。该可执行文件包括所有类、依赖项、库,以及更重要的是,所有虚拟机功能,如内存管理、线程管理等。虚拟机功能被打包成一个名为 Substrate VM 的运行时。我们在第三章的Substrate VM(Graal AOT 和原生图像)部分简要介绍了 Substrate VM。在本章中,我们将更深入地了解原生图像。我们将通过一个示例学习如何构建、运行和优化原生图像。
原生图像只能执行静态代码优化,并且没有即时编译器所具有的运行时优化的优势。我们将通过使用运行时分析数据来探索基于配置的优化,这是一种可以用来优化原生图像的技术。
在本章中,我们将涵盖以下主题:
-
理解如何构建和运行原生图像
-
理解原生图像的架构以及编译过程是如何工作的
-
探索各种工具、编译器和运行时配置,以分析和优化原生图像的构建和执行方式
-
理解如何使用配置指导优化(PGO)来优化原生图像
-
理解原生图像的限制以及如何克服这些限制
-
理解原生图像如何管理内存
到本章结束时,你将清楚地理解 Graal 的提前编译,并在构建和优化原生图像方面获得实践经验。
技术要求
我们将使用以下工具和示例代码来探索和理解 Graal 的提前编译:
-
native-image工具。 -
GraalVM 仪表板:在本章中,我们将使用 GraalVM 仪表板来分析我们创建的原生图像。
-
访问 GitHub:有一些示例代码片段,可以在 Git 仓库中找到。代码可以从
github.com/PacktPublishing/Supercharge-Your-Applications-with-GraalVM/tree/main/Chapter05下载。 -
本章的“代码在行动”视频可以在
bit.ly/3ftfzNr.找到。
构建原生图像
在本节中,我们将使用 Graal Native Image 构建器(native-image)构建一个原生图像。让我们先安装 Native Image 构建器。
可以使用以下命令通过 GraalVM 更新器安装native-image:
gu install native-image
工具直接安装在GRAALVM_HOME的/bin文件夹中。
现在让我们创建FibonacciCalculator的本地镜像,从第四章的Graal 即时编译器配置部分。
要创建本地镜像,编译 Java 文件并运行native-image FibonacciCalculator –no-fallback -noserver。以下截图显示了运行命令后的输出:

图 5.1 – FibonacciCalculator – 生成本地镜像控制台输出
本地镜像编译需要时间,因为它必须执行大量的静态代码分析以优化生成的镜像。以下图表显示了本地镜像构建器执行的预编译器编译流程:

图 5.2 – 本地镜像管道流程
让我们努力更好地理解这张图片:
-
预编译器加载所有应用程序代码、依赖库和类,并将它们与 Java 开发工具包类和基底虚拟机类一起打包。
-
基底虚拟机具有运行应用程序所需的所有虚拟机功能。这包括内存管理、垃圾回收、线程管理、调度等。
-
然后编译器在构建本地镜像之前对代码执行以下优化:
a.
META-INF/native-image/reflect-config.json) 它还可能配置其他动态功能,如 JNI、代理等。配置文件需要放在CLASSPATH中,然后编译器会负责将这些功能包含在最终的本地镜像中。b.
static和static final字段、enum常量、java.lang.Class对象等。 -
最终的本地镜像包含代码部分,其中放置了最终优化的二进制代码,以及在可执行文件的数据部分,写入堆镜像。这将有助于快速加载本地镜像。
这里是构建时和运行时点到分析和区域分析的高级流程:

图 5.3 – 本地镜像管道 – 点到分析和区域分析
让我们详细理解这张图片:
-
在构建时,点到分析扫描应用程序代码、依赖项和 JDK 以查找可到达的代码。
-
区域分析捕获堆区域元数据,包括区域映射和区域进入/退出。区域分析还使用可到达的代码来识别哪些静态元素需要初始化。
-
代码是根据可到达的代码和区域元数据生成的。
-
在运行时,堆分配器使用区域映射预先分配内存,而区域管理器处理进入和退出。
现在,让我们通过命令行运行原生图像,执行 ./fibonaccicalculator。以下是一个执行原生图像的截图:

图 5.4 – FibonacciCalculator – 运行 FibonacciCalculator 原生图像控制台输出
预编译编译的一个最大的缺点是编译器永远不会对运行时进行配置以优化代码,而即时编译(JIT)则可以很好地完成这项工作。为了将两者的优点结合起来,我们可以使用 PGO 技术。我们在 第三章 的 GraalVM 架构 中简要介绍了 PGO。让我们看看它的实际应用,并更深入地理解它。
使用 GraalVM 仪表板分析原生图像
要深入了解点分析(points-to analysis)和区域分析(region analysis)的工作原理,我们可以使用 GraalVM 仪表板。在本节中,我们将构建原生图像时创建转储,并使用 GraalVM 可视化原生图像构建器执行点分析和区域分析。
在 第四章 的 调试和监控应用程序 部分([B16878_04_Final_SK_ePub.xhtml#_idTextAnchor077]),Graal 即时编译器 中,我们简要介绍了 GraalVM 仪表板。GraalVM 仪表板是一个专门针对原生图像的非常强大的工具。在本节中,我们将生成 FibonnacciCalculator 示例的仪表板转储,并探讨如何使用 GraalVM 仪表板深入了解原生图像。
要生成仪表板转储,我们必须使用 -H:DashboardDump=<文件名> 标志。对于我们的 FibonacciCalculator,我们使用以下命令:
native-image -H:DashboardDump=dashboard -H:DashboardAll FibonacciCalculator
以下截图显示了此命令生成的输出。该命令创建了一个 dashboard.bgv 文件:

图 5.5 – FibonacciCalculator – 生成仪表板转储控制台输出
我们还使用了 -H:DashboardAll 标志来转储所有参数。以下是可以使用的替代标志:
-
-H:+DashboardHeap: 此标志仅转储图像堆。 -
-H:+DashboardCode: 此标志生成代码大小,按方法细分。 -
-H:+DashboardPointsTo: 此标志创建原生图像构建器执行的点分析转储。
现在,让我们加载这个 dashboard.bgv 文件,并分析结果。我们需要将 dashboard.bgv 文件上传到 GraalVM 仪表板。打开浏览器并访问 www.graalvm.org/docs/tools/dashboard/。
我们应该看到以下屏幕:

图 5.6 – GraalVM 仪表板主页
点击左上角的 + 加载数据 按钮。您将得到一个如图所示的对话框:

图 5.7 – 上传仪表板转储文件窗口
点击我们生成的 dashboard.bgv 文件。您将立即看到仪表板,如图 5.8 所示。您将在左侧找到两个生成的报告 – 代码大小拆分 和 堆大小拆分。
理解代码大小拆分报告
代码大小拆分报告提供了各种类别的代码大小,这些代码被分类到块中。块的大小代表代码的大小。以下图显示了当我们选择上一节中生成的 dashboard.bgv 时初始仪表板屏幕。通过悬停在块上,我们可以通过方法获得更清晰的尺寸拆分。我们可以双击这些块来深入了解:

图 5.8 – 代码大小拆分仪表板
以下截图显示了当我们双击 FibonacciCalculator 块时看到的报告。再次,我们可以双击调用图:

图 5.9 – 代码大小拆分 – 详细报告
以下截图显示了调用图。这有助于我们了解原生图像构建器所执行的点分析。如果识别出任何未使用的类或方法的依赖关系,我们可以利用这些信息来识别优化源代码的机会:

图 5.10 – 代码点分析依赖关系报告
现在,让我们看看堆大小拆分。
堆大小拆分
堆大小拆分提供了对堆分配的详细洞察,同时也深入探讨了堆。我们可以双击这些块来了解这些堆分配。以下截图显示了 FibonacciCalculator 的堆大小拆分报告:

图 5.11 – 堆大小拆分仪表板
在本节中,我们探讨了如何使用 GraalVM 仪表板分析原生图像。现在,让我们看看如何使用 PGO 优化我们的原生图像。
理解 PGO
使用 PGO,我们可以运行原生图像,并选择生成运行时配置文件。JVM 创建一个配置文件,.iprof,它可以用来重新编译原生图像,以进一步优化它。以下图表(回想一下在 第三章 的 配置文件引导优化 (PGO) 部分,GraalVM 架构) 展示了 PGO 的工作原理:

图 5.12 – 原生图像 – 配置文件引导优化流程
前面的图表显示了使用 PGO 的本地图像编译管道流程。让我们更好地理解这个流程:
-
初始本地图像通过传递
–pgo-instrument标志参数,在打包本地图像时进行测量,以创建配置文件。这将生成一个带有测量代码的本地图像。 -
当我们用多个输入运行本地图像时,本地图像会创建一个配置文件。这个配置文件是与
.iprof扩展名相同的文件,位于同一目录下。 -
一旦我们运行了所有用例,为了确保创建的配置文件覆盖了所有路径,我们可以通过传递
.iprof文件作为参数以及--pgo参数来重新构建本地图像。 -
这将生成优化后的本地图像。
现在,让我们构建 FibonacciCalculator 类的优化本地图像。让我们首先通过运行以下命令来创建一个经过测量的本地图像:
java -Dgraal.PGOInstrument=fibonaccicalculator.iprof -Djvmci.CompilerIdleDelay-0 FibonacciCalculator
此命令将使用配置信息构建本地图像。以下截图显示了构建本地图像的输出:

图 5.13 – FibonacciCalculator – 生成 PGO 配置文件控制台输出
这将在当前目录下生成 fibonaccicalculator.iprof 文件。现在,让我们使用以下命令使用此配置文件重新构建我们的本地图像:
native-image –pgo=fibonaccicalculator.iprof FibonacciCalculator
这将使用配置文件重新构建本地图像,生成最优的可执行文件。以下截图显示了使用配置文件构建本地图像时的输出:

图 5.14 – FibonacciCalculator – 生成基于配置文件优化的本地图像控制台输出
现在,让我们执行优化后的文件。以下截图显示了运行优化后的本地图像时的输出结果:

图 5.15 – FibonacciCalculator – 运行 PGO 图像控制台输出
如您所见,它比原始本地图像快得多。现在,让我们比较一下值。以下图表显示了压缩率:

图 5.16 – FibonacciCalculator – 本地图像与 PGO 本地图像比较
如您所见,PGO 的性能更快、更好。
虽然这一切都很好,但如果与 JIT 进行比较,我们会发现本地图像的表现并不那么出色。让我们将其与 JIT(Graal 和 Java HotSpot)进行比较。以下图表显示了比较结果:

图 5.17 – FibonacciCalculator – Graal JIT 与 Java HotSpot 与本地图像与 PGO 本地图像比较
这突出了关键点之一,即原生图像并不总是最优的。在这种情况下,这肯定不是,因为我们使用大型数组进行堆分配。这直接影响了性能。这是作为开发者,优化代码的重要领域之一。原生图像使用 Serial GC,因此不建议为大型堆使用原生图像。
让我们优化代码并看看原生图像是否比 JIT 运行得更快。以下是优化后的代码,它执行了完全相同的逻辑,但使用了更少的堆:
public class FibonacciCalculator2{
public long findFibonacci(int count) {
int fib1 = 0;
int fib2 = 1;
int currentFib, index;
long total = 0;
for(index=2; index < count; ++index ) {
currentFib = fib1 + fib2;
fib1 = fib2;
fib2 = currentFib;
total += currentFib;
}
return total;
}
public static void main(String args[]) {
FibonacciCalculator2 fibCal = new FibonacciCalculator2();
long startTime = System.currentTimeMillis();
long now = 0;
long last = startTime;
for (int i = 1000000000; i < 1000000010; i++) {
fibCal.findFibonacci(i);
now = System.currentTimeMillis();
System.out.printf("%d (%d ms)%n", i , now - last);
last = now;
}
long endTime = System.currentTimeMillis();
System.out.printf(" total: (%d ms)%n", System.currentTimeMillis() – startTime);
}
}
下面是使用 Graal JIT 和原生图像运行此代码的最终结果。正如您将在下面的屏幕截图中所看到的,原生图像在启动时不需要任何时间,并且比 JIT 运行得快得多。让我们用 Graal JIT 运行优化后的代码。以下是运行优化代码后的输出:

图 5.18 – FibonacciCalculator2 – 使用 Graal 运行优化后的代码
现在,让我们运行优化代码的原生图像。下一个屏幕截图显示了运行原生图像后的输出:

图 5.19 – FibonacciCalculator2 – 作为原生图像运行优化后的代码
如果绘制性能图表,您可以看到显著的改进:

图 5.20 – FibonacciCalculator2 – Graal JIT 与原生图像对比
理解 AOT 编译的限制并采用正确的方法是很重要的。让我们快速浏览一下构建原生图像的一些编译器配置。
原生图像配置
原生图像构建是高度可配置的,并且始终建议在native-image.properties文件中提供所有构建配置。由于native-image工具以 JAR 文件作为输入,建议将native-image.properties打包在 JAR 文件中的META-INF/native-image/<unique-application-identifier>内。使用唯一的应用程序标识符以避免任何资源冲突。这些路径必须是唯一的,因为它们将在CLASSPATH上配置。native-image工具在构建时使用CLASSPATH来加载这些资源。除了native-image.properties之外,还有各种其他配置文件可以打包。在本节中,我们将介绍一些重要的配置。
以下是native-image.properties文件的典型格式,以及对该属性文件中每个部分的解释:
Requires = <space separated list of languages that are required>
JavaArgs = <Javaargs that we want to pass to the JVM>
Args = <native-image arguments that we want to pass>
-
Requires:Requires属性用于列出所有语言示例,例如language:llvmlanguage:python。 -
JavaArgs:我们可以使用此属性传递常规 Java 参数。 -
ImageName:此参数可用于为生成的原生图像提供自定义名称。默认情况下,原生图像的名称与 JAR 文件或 Mainclass 文件(全部为小写字母)相同。例如,我们的FibonnaciCalculator.class生成fibonaccicalculator。 -
Args:这是最常用的属性。它可以用来提供原生图像的参数。参数也可以从命令行传递,但从配置管理的角度来看,将它们列在native-image.properties文件中会更好,这样就可以将其放入 Git(或任何源代码仓库)并跟踪任何更改。以下表格解释了一些通常使用的参数:

请参阅 www.graalvm.org/reference-manual/native-image/Options/ 以获取选项的完整列表。
主机选项和资源配置
我们可以使用各种参数来配置各种资源。这些资源声明通常配置在外部 JSON 文件中,并且可以使用各种 -H: 标志将其指向这些资源文件。语法是 -H<Resource Flag>=${.}/jsonfile.json。以下表格列出了使用的一些重要参数:


native-images.properties 捕获所有配置参数,并且通过 native-image.properties 文件传递配置是一个好习惯,因为它在源代码配置管理工具中易于管理。
GraalVM 随附一个代理,该代理在运行时跟踪 Java 程序的动态特性。这有助于识别和配置具有动态特性的原生图像构建。要使用原生图像代理运行 Java 应用程序,我们需要传递 -agentlib:native-image-agent=config-output-dir=<path to config dir>。代理跟踪执行并拦截查找类、方法、资源和代理的调用。然后代理在作为参数传递的配置目录中生成 jni-config.json、reflect-config.json、proxy-config.json 和 resource-config.json。运行应用程序多次,使用不同的测试用例,以确保完整代码被覆盖,并且代理能够捕获大多数动态调用是一个好习惯。当我们运行迭代时,使用 -agentlib:native-image-agent=config-merge-dir=<path to config dir> 非常重要,这样配置文件就不会被覆盖,而是合并。
我们可以使用原生图像生成 Graal 图,以分析原生图像的运行情况。在下一节中,我们将探讨如何生成这些 Graal 图。
为原生图像生成 Graal 图
即使是原生图像也可以生成 Graal 图。Graal 图可以在构建时或运行时生成。让我们在本节中使用我们的 FibonnaciCalculator 应用程序来探索这个功能。
让我们使用此命令生成FibonacciCalculator的转储:
native-image -H:Dump=1 FibonacciCalculator
以下为输出:
native-image -H:Dump=1 FibonacciCalculator
[fibonaccicalculator:54143] classlist: 811.75 ms, 0.96 GB
[fibonaccicalculator:54143] (cap): 4,939.64 ms, 0.96 GB
[fibonaccicalculator:54143] setup: 6,923.28 ms, 0.96 GB
[fibonaccicalculator:54143] (clinit): 155.70 ms, 2.29 GB
[fibonaccicalculator:54143] typeflow): 3,841.07 ms, 2.29 GB
[fibonaccicalculator:54143] (objects): 3,235.92 ms, 2.29 GB
[fibonaccicalculator:54143] (features): 169.55 ms, 2.29 GB
[fibonaccicalculator:54143] analysis: 7,550.64 ms, 2.29 GB
[fibonaccicalculator:54143] universe: 295.75 ms, 2.29 GB
[fibonaccicalculator:54143] (parse): 829.58 ms, 3.18 GB
[fibonaccicalculator:54143] (inline): 1,357.72 ms, 3.18 GB
[Use -Dgraal.LogFile=<path> to redirect Graal log output to a file.]
Dumping IGV graphs in /graal_dumps/2021.02.28.20.32.24.880
Dumping IGV graphs in /graal_dumps/2021.02.28.20.32.24.880
[fibonaccicalculator:54143] (compile): 13,244.17 ms, 4.74 GB
[fibonaccicalculator:54143] compile: 16,249.17 ms, 4.74 GB
[fibonaccicalculator:54143] image: 1,816.98 ms, 4.74 GB
[fibonaccicalculator:54143] write: 430.54 ms, 4.74 GB
[fibonaccicalculator:54143] [total]: 34,247.73 ms, 4.74 GB
此命令为每个初始化的类生成大量图表。我们可以使用-H:MethodFilter标志指定我们想要为其生成图表的类和方法。命令看起来可能像这样:
native-image -H:Dump=1 -H:MethodFilter=FibonacciCalculator.main FibonacciCalculator
请参阅第四章的Graal 中间表示部分,了解如何阅读这些图表并理解优化代码的机会。对于原生图像,优化源代码至关重要,因为我们没有像即时编译器那样的运行时优化。
理解原生图像如何管理内存
原生图像与 Substrate VM 捆绑在一起,该 VM 具有管理内存的功能,包括垃圾收集。正如我们在构建原生图像部分所看到的,堆分配是图像创建的一部分,以加快启动速度。这些是在构建时初始化的类。请参阅图 5.3以了解原生图像构建器在执行静态区域分析后如何初始化堆区域。在运行时,垃圾收集器管理内存。原生图像构建器支持两种垃圾收集配置。以下小节将介绍这两种垃圾收集配置。
序列垃圾收集器
序列垃圾收集器(GC)是默认集成到原生图像中的。这在社区版和企业版中都是可用的。此垃圾收集器针对低内存占用和小堆大小进行了优化。我们可以使用--gc=serial标志显式使用序列垃圾收集器。序列垃圾收集器是 GC 的简单实现。
序列垃圾收集器将堆分为两个区域,即年轻代和老年代。以下图示显示了序列垃圾收集器的工作原理:

图 5.21 – 序列垃圾收集器堆架构
年轻代用于新对象。当年轻代块满且所有未使用的对象被回收时,会触发。当老年代块满时,会触发完全收集。年轻代收集运行得更快,而在运行时完全收集更耗时。可以使用-XX:PercentTimeInIncrementalCollection参数来调整此行为。
默认情况下,此百分比是 50%。这可以增加到减少完全收集的次数,从而提高性能,但会对内存大小产生负面影响。根据内存分析,在测试应用程序时,我们可以优化此参数以获得更好的性能和内存占用。以下是如何在运行时传递此参数的示例:
./fibonaccicalculator -XX:PercentTimeInIncrementalCollection=40
此参数也可以在构建时传递:
native-image --gc=serial -R:PercentTimeInIncrementalCollection=70 FibonacciCalculator
可以使用其他参数来进行微调,例如-XX:MaximumYoungGenerationSizePercent。这个参数可以用来调整年轻代块应该占整体堆的最大百分比。
序列 GC 是单线程的,对于小堆来说效果很好。以下图示展示了序列 GC 的工作方式。应用程序线程被暂停以回收内存。这被称为停止世界事件。在这段时间内,垃圾收集线程运行并回收内存。如果堆大小很大且有很多线程运行,这将对应用程序的性能产生影响。序列 GC 非常适合小进程和小堆大小。

图 5.22 – 序列 GC 堆流程
默认情况下,序列 GC 假设在启动 GC 线程之前堆大小为 80%,这可以通过-XX:MaximumHeapSizePercent标志来更改。还有其他标志可以用来微调序列 GC 的性能。
G1 垃圾收集器
G1 垃圾收集器是更近期的、更高级的垃圾收集器实现。这仅在企业版中可用。可以使用--gc=G1标志启用 G1 垃圾收集器。G1 提供了吞吐量和延迟之间的正确平衡。吞吐量是运行代码的平均时间与 GC 的时间之比。更高的吞吐量意味着我们有更多的 CPU 周期用于代码,而不是 GC 线程。延迟是停止世界事件所需的时间或暂停代码执行的时间。延迟越少,对我们来说越好。G1 的目标是高吞吐量和低延迟。以下是它的工作方式。
G1 将整个堆划分为小区域。G1 运行并发线程以查找所有活动对象,Java 应用程序永远不会暂停,并跟踪区域间的所有指针,并尝试收集区域以使程序中的暂停时间更短。G1 也可能移动活动对象并将它们合并到区域中,并尝试使区域为空。

图 5.23 – G1 GC 堆流程
之前的图示展示了 G1 GC 如何通过划分区域来工作。对象分配到区域是基于尝试在空区域分配内存,并通过将对象合并到区域中(如分区和去分区)来尝试清空区域。其理念是优化管理和收集区域。
G1 垃圾收集器的占用空间比序列 GC 大,适用于运行时间更长、堆大小更大的情况。可以使用各种参数来微调 G1 GC 的性能。以下列出了一些参数(-H是在构建镜像时传递的参数,-XX是在运行镜像时传递的):
-
-H:G1HeapRegionSize:这是每个区域的大小。 -
-XX:MaxRAMPercentage:用作堆大小的物理内存大小的百分比。 -
-XX:ConcGCThreads:并发 GC 线程的数量。这需要优化以获得最佳性能。 -
-XX:G1HeapWastePercent:垃圾收集器达到此百分比时停止声明。这将允许更低的延迟和更高的吞吐量,但是,设置一个最佳值是至关重要的,因为如果它太高,那么对象将永远不会被收集,应用程序的内存占用将始终很高。
选择合适的垃圾回收器和配置对于应用程序的性能和内存占用至关重要。
管理堆大小和生成堆转储
可以使用以下在运行原生图像时传递的运行时参数手动设置堆大小。-Xmx设置最大堆大小,-Xms设置最小堆大小,-Xmn设置年轻代区域的大小,以字节为单位。以下是如何在运行时使用这些参数的示例:
./fibonaccicalculator -Xms2m -Xmx10m -Xmn1m
在构建时,我们可以传递参数来配置堆大小。这是一个关键的配置,必须非常小心地进行,因为这将直接影响原生图像的内存占用和性能。以下命令是一个配置最小堆大小、最大堆大小和堆的最大新大小的示例:
native-image -R:MinHeapSize=2m -R:MaxHeapSize=10m -R:MaxNewSize=1m FibonacciCalculator
堆转储对于调试任何内存泄漏和内存管理问题至关重要。我们通常使用 VisualVM 等工具进行此类堆转储分析。原生图像不是使用-H:+AllowVMInspection标志构建的。这将创建一个原生图像,当发送 USR1 信号(sudo kill -USR1或-SIGUSR1或QUIT/BREAK键)时可以生成堆栈转储,当发送 USR2 信号(sudo kill -USR2或-SIGUSR2 – 您可以使用kill -l命令检查确切的信号)时可以生成运行时编译信息转储。此功能仅适用于企业版。
我们还可以通过在需要时调用org.graalvm.nativeimage.VMRuntime#dumpHeap来程序化地创建堆转储。
构建静态原生图像和原生共享库
静态原生图像是静态链接的二进制文件,在运行时不需要任何额外的依赖库。当我们将微服务应用程序构建为原生图像时,这些图像非常有用,因为它们可以轻松地打包到 Docker 中,无需担心依赖关系。静态图像最适合构建基于容器的微服务。
在撰写本书时,此功能仅适用于 Java 11 的 Linux AMD64。请参阅www.graalvm.org/reference-manual/native-image/StaticImages/以获取最新更新和构建静态原生图像的过程。
原生图像构建器还会构建共享库。有时您可能希望将代码作为共享库创建,该库被某些其他应用程序使用。为此,您必须传递 –shared 标志来构建共享库,而不是可执行库。
调试原生图像
调试原生图像需要构建包含调试信息的图像。我们可以使用 -H:GenerateDebugInfo=1。以下是一个为 FibonnacciCalculator 使用此参数的示例:
native-image -H:GenerateDebugInfo=1 FibonacciCalculator
生成的图像以 GNU 调试器 (GDB) 的形式包含调试信息。这可以用于在运行时调试代码。以下显示了运行前述命令的输出:
native-image -H:GenerateDebugInfo=1 FibonacciCalculator
[fibonaccicalculator:57833] classlist: 817.01 ms, 0.96 GB
[fibonaccicalculator:57833] (cap): 6,301.03 ms, 0.96 GB
[fibonaccicalculator:57833] setup: 9,946.35 ms, 0.96 GB
[fibonaccicalculator:57833] (clinit): 147.54 ms, 1.22 GB
[fibonaccicalculator:57833] (typeflow): 3,642.34 ms, 1.22 GB
[fibonaccicalculator:57833] (objects): 3,164.39 ms, 1.22 GB
[fibonaccicalculator:57833] (features): 181.00 ms, 1.22 GB
[fibonaccicalculator:57833] analysis: 7,282.44 ms, 1.22 GB
[fibonaccicalculator:57833] universe: 304.43 ms, 1.22 GB
[fibonaccicalculator:57833] (parse): 624.60 ms, 1.22 GB
[fibonaccicalculator:57833] (inline): 989.65 ms, 1.67 GB
[fibonaccicalculator:57833] (compile): 8,486.97 ms, 3.15 GB
[fibonaccicalculator:57833] compile: 10,625.01 ms, 3.15 GB
[fibonaccicalculator:57833] image: 869.81 ms, 3.15 GB
[fibonaccicalculator:57833] debuginfo: 1,078.95 ms, 3.15 GB
[fibonaccicalculator:57833] write: 2,224.22 ms, 3.15 GB
[fibonaccicalculator:57833] [total]: 33,325.95 ms, 3.15 GB
这将生成一个 sources 目录,其中包含由原生图像构建器生成的缓存。此缓存将 JDSK、GraalVM 和应用程序类引入以帮助调试。以下是列出 sources 目录内容的输出:
$ ls -la
total 8
drwxr-xr-x 9 vijaykumarab staff 288 17 Apr 09:01 .
drwxr-xr-x 10 vijaykumarab staff 320 17 Apr 09:01 ..
-rw-r--r-- 1 vijaykumarab staff 1240 17 Apr 09:01 FibonacciCalculator.java
drwxr-xr-x 3 vijaykumarab staff 96 17 Apr 09:01 com
drwxr-xr-x 5 vijaykumarab staff 160 17 Apr 09:01 java.base
drwxr-xr-x 3 vijaykumarab staff 96 17 Apr 09:01 jdk.internal.vm.compiler
drwxr-xr-x 3 vijaykumarab staff 96 17 Apr 09:01 jdk.localedata
drwxr-xr-x 3 vijaykumarab staff 96 17 Apr 09:01 jdk.unsupported
drwxr-xr-x 3 vijaykumarab staff 96 17 Apr 09:01 org.graalvm.sdk
要调试原生图像,我们需要 gdb 工具。有关如何在您的目标机器上安装 gdb 的信息,请参阅www.gnu.org/software/gdb/。一旦正确安装,我们应该能够通过执行 gdb 命令进入 gdb 壳。以下显示了典型的输出:
$ gdb
GNU gdb (GDB) 10.1
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-apple-darwin20.2.0".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".(gdb)
我们需要指向我们在上一步中生成源文件的目录。我们可以通过执行以下命令来完成:
set directories /<sources directory>/jdk:/ <sources directory>/graal:/ <sources directory>/src
一旦环境设置完成,我们就可以使用 gdb 来设置断点和调试。有关如何使用 gdb 调试可执行文件的详细文档,请参阅www.gnu.org/software/gdb/documentation/。
在撰写本书时,调试信息可用于执行断点、单步执行、堆栈回溯、打印原始值、对象的转换和打印、路径表达式以及通过方法名和静态数据引用。有关更多详细信息,请参阅www.graalvm.org/reference-manual/native-image/DebugInfo/。
Graal AOT (Native Image) 的局限性
在本节中,我们将探讨 Graal AOT 和原生图像的一些局限性。
Graal 的时间前编译假设了一个封闭世界的静态分析。它假设在运行时可达的所有类在构建时都是可用的。这对编写任何需要动态加载的代码(如反射、JNI、代理等)有直接影响。然而,Graal AOT 编译器(native-image)提供了一种以 JSON 清单文件的形式提供此元数据的方法。这些文件可以与 JAR 文件一起打包,作为编译器的输入:
-
动态加载类:在运行时加载的类,在构建时对 AOT 编译器不可见,需要在配置文件中指定。这些配置文件通常保存在
META-INF/native-image/下,应在CLASSPATH中。如果在编译配置文件时找不到类,它将抛出ClassNotFoundException。 -
反射:任何调用
java.lang.reflectAPI 来列出方法和字段或使用反射 API 调用它们的调用都必须在META-INF/native-image/下的reflect-config.json文件中进行配置。编译器试图通过静态分析来识别这些反射元素。 -
动态代理:生成的动态代理类是
java.lang.reflect.Proxy的实例,需要在构建时定义。代理配置需要在proxy-config.json中配置接口。 -
jni-config.json. -
序列化:Java 序列化也会动态访问大量的类元数据。即使是这些访问也需要提前配置。
你可以在此处找到有关其他限制的更多详细信息:www.graalvm.org/reference-manual/native-image/Limitations/。
GraalVM 容器
GraalVM 还打包为 Docker 容器。可以直接从 Docker Registry (ghcr.io)拉取,也可以用作构建自定义镜像的基础镜像。以下是一些使用 GraalVM 容器的关键命令:
-
拉取 Docker 镜像:
docker pull ghcr.io/graalvm/graalvm-ce:latest -
运行容器:
docker run -it ghcr.io/graalvm/graalvm-ce:latest bash -
在 Dockerfile 中作为基础镜像使用:
FROM ghcr.io/graalvm/graalvm-ce:latest
当我们谈到在 GraalVM 上构建微服务时,我们将在第九章 GraalVM Polyglot – LLVM, Ruby, and WASM中进一步探讨关于 GraalVM 容器的更多内容。
摘要
在本章中,我们详细介绍了 Graal 的即时编译器和提前编译器。我们取了示例代码并查看 Graal JIT 如何执行各种优化。我们还详细介绍了如何理解 Graal 图。这是在开发期间分析和识别我们可以做的优化,以加快运行时 Graal JIT 编译速度的关键知识。
本章提供了关于如何构建原生图像以及如何使用配置文件引导优化来优化原生图像的详细说明。我们取了示例代码并编译了原生图像,还发现了原生图像的内部工作原理。我们识别了可能导致原生图像运行速度比即时编译器慢的代码问题。我们还涵盖了原生图像的限制以及何时使用原生图像。我们探索了各种构建时间和运行时配置以优化构建和运行原生图像。
在下一章中,我们将深入了解 Truffle 语言实现框架以及如何构建多语言应用。
问题
-
原生镜像是如何创建的?
-
什么是指针分析?
-
什么是区域分析?
-
什么是串行 GC 和 G1 GC?
-
如何优化原生镜像?什么是 PGO?
-
原生镜像有哪些限制?
进一步阅读
-
GraalVM Enterprise 版本 (
docs.oracle.com/en/graalvm/enterprise/19/index.html) -
Graal VM Native Image 文档 (
www.graalvm.org/reference-manual/native-image/)
第三部分:使用 Graal 的多语言
本节解释了我们可以如何使用 GraalVM 作为多语言虚拟机,并通过实际操作演示多语言互操作性是如何工作的,以及 Truffle 如何帮助 Java、Python 和 JavaScript 的多语言支持。本节包括以下章节:
-
第六章, Truffle – 概述
-
第七章, GraalVM 多语言 – JavaScript 和 Node.js
-
第八章, GraalVM 多语言 – Truffle、Python 和 R 上的 Java
-
第九章, Graal 多语言 – LLVM、Ruby 和 WASM
第八章:Truffle – 概述
多语言开发支持是 GraalVM 最大的特性之一。在第四章 Graal 即时编译器和第五章 Graal 预编译器及原生图像中,我们详细介绍了 Graal 如何在构建时间和运行时优化代码。在前面的所有章节中,我们只使用了 Java。然而,GraalVM 将其大多数高级特性扩展到了其他编程语言。GraalVM 提供了一个名为Truffle 语言实现框架(通常称为Truffle)的语言实现框架。
GraalVM 不仅为 Java、Groovy、Kotlin 和 Scala 等 JVM 语言提供高性能运行时,还支持 JavaScript、Ruby、Python、R、WebAssembly 以及实现 Truffle 的 LLVM 语言等非 JVM 语言。还有更多语言正在 Truffle 上实现。
本章从概念上阐述了 Truffle 如何帮助客户端语言开发者,并提供了一个精心设计、高性能的框架,用于在 GraalVM 上使用客户端语言构建应用程序。本章不会过多涉及如何使用 Truffle 在客户端语言中编写代码。这只是为了描述 Truffle 的架构和概念,以便您能够理解后续章节中如何在 GraalVM 上实现非 JVM 语言。
本章将涵盖以下主题:
-
探索 Truffle 语言实现框架
-
探索 Truffle 解释器/编译器管道
-
学习 Truffle DSL
-
理解 Truffle 如何支持互操作性
-
理解 Truffle 仪表
-
使用 Truffle 进行即时编译
-
使用启动器选项优化 Truffle 解释器性能
-
SimpleLanguage 和 Simple Tool
到本章结束时,您将很好地理解 Truffle 的架构以及 Truffle 如何为其他编程语言提供在 GraalVM 上运行的框架。
探索 Truffle 语言实现框架
在第三章中,GraalVM 架构部分,我们简要介绍了 Truffle 的架构。Truffle 是一个开源库,提供了一个框架来实现语言解释器。Truffle 帮助运行实现该框架的客户端编程语言,以利用 Graal 编译器的功能生成高性能代码。Truffle 还提供了一个名为 SimpleLanguage 的参考实现,以指导开发者为其语言编写解释器。Truffle 还提供了一个工具框架,有助于集成和利用一些现代的诊断、调试和分析工具。
让我们了解 Truffle 如何融入 GraalVM 整体生态系统。除了语言之间的互操作性外,Truffle 还提供了可嵌入性。互操作性允许在不同语言之间调用代码,而可嵌入性允许将不同语言编写的代码嵌入到同一程序中。
语言互操作性对于以下原因至关重要:
-
不同的编程语言是为了解决不同的问题而构建的,它们各自具有自己的优势。例如,我们广泛使用 Python 和 R 进行机器学习和数据分析,而使用 C/C+ 进行高性能数学运算。想象一下,如果我们直接重用代码,无论是通过从宿主语言(如 Java)调用代码,还是将代码嵌入到宿主语言中,这将增加代码的可重用性,并允许我们使用适合当前任务的适当语言,而不是在不同语言中重写逻辑。
-
如果我们拥有多编程语言互操作性的功能,那么在从一种语言迁移到另一种语言的重大迁移项目中,我们可以分阶段进行。这大大降低了迁移的风险。
下图展示了如何在 GraalVM 上运行用其他语言编写的应用程序:

图 6.1 – Truffle 堆栈
在图中,我们可以看到 GraalVM,这是我们在前几章中介绍过的 JVM 和 Graal JIT 编译器。在其之上,我们有 Truffle 框架。Truffle 有两个主要组件。它们如下:
-
Truffle API:Truffle API 是任何客语言程序员都可以使用的语言实现框架,用于为各自的语言实现 Truffle 解释器。Truffle 提供了一个复杂的 API 用于抽象语法树(AST)重写。客语言被转换为 AST 以在 GraalVM 上进行优化和运行。Truffle API 还有助于提供实现 Truffle API 的语言之间的互操作性框架。
-
Truffle 优化器:Truffle 优化器为部分评估的投机优化提供了一个额外的优化层。我们将在后续章节中更详细地介绍这一点。
在 Truffle 层之上,我们有客语言。这是 JavaScript、R、Ruby 等实现 Truffle 语言实现框架的语言。最后,我们有在客语言运行时之上运行的应用程序。在大多数情况下,应用程序开发者不必担心更改代码以在 GraalVM 上运行。Truffle 通过提供中间层使其无缝。以下图显示了 GraalVM 和 Truffle 生态系统的详细堆栈视图:

图 6.2 – Truffle 和 Graal 详细堆栈视图
此图简单地展示了 Truffle 如何在非 JVM 语言和 GraalVM 之间充当一层。让我们详细了解这一点。
Truffle 提供 API,供各个解释器实现,将代码重写为 AST。AST 表示随后被转换为 Graal 中间表示,以便 Graal 执行和即时优化。客语言在各自的 Truffle 解释器实现之上运行。
让我们看看这些不同层如何交互,以及 Truffle 如何帮助客语言在 GraalVM 上运行。
探索 Truffle 解释器/编译器管道
Truffle 提供 API,供各个解释器实现,将代码重写为 AST。AST 表示随后被转换为 Graal 中间表示,以便 Graal 执行和即时优化。客语言在各自的 Truffle 解释器实现之上运行。

图 6.4 – AST 特殊化说明
在图象的左侧,Truffle 解释器从通用的 AST 节点开始。根据分析器,解释器理解它主要是一个特定类型的值,比如说一个整数。它将重写具有整数类型的节点和具有整数操作的运算节点。这优化了 AST 的执行。在最左侧的表示中,+可能意味着字符串的连接、整数、长整数或浮点数的加法,或者任何其他将运算减少到最右侧表示的多态执行,在那里它非常明显是一个整数。如果假设在未来被证明是错误的,则在那里放置一个守卫检查。如果操作数恰好是浮点数或某种其他类型,则将调用去优化,这可能会将其带回到最左侧的表示。Truffle 解释器将再次分析以确定要应用的正确类型特殊化。它可能确定它更像是长整数或双精度浮点数,因此它可能将 AST 重写为双精度浮点数并优化。类型特殊化应用于局部变量、返回类型、操作和字段。
基于类型特殊化的 AST 重写为性能提供了显著的提升,当我们达到 Graal 执行的更高级优化时,我们有一个非常稳定的 AST。
由于 AST 解释器是用 Java 实现的,因此 execute() 节点方法被编写来处理通用对象。类型专业化还有助于通过用专门的 executeInt() 方法替换 execute() 方法,以及通过用原始类型(int、double 等)替换包装器实现(Integer、Double 等)来减少 CPU 的装箱和拆箱负载。这种技术有时被称为 装箱消除。
一旦解释器发现没有节点重写,这意味着 AST 已经稳定。然后,将代码编译成机器码,并将所有虚拟调用内联为特定的调用。然后,将此代码传递给 Graal JIT 编译器在运行时进行进一步优化。这被称为 部分评估。如果假设有效,则将守卫嵌入到代码中。当任何假设无效时,守卫代码将执行回 AST 解释器,在那里节点重写将再次发生。这被称为 转移到解释器。
部分评估
调用这些 execute() 方法是虚拟调度,这会对性能产生显著的开销。当 Truffle 识别出稳定的代码,即没有更多的 AST 重写发生且代码中有大量调用时,它会执行部分评估来提高该代码的执行性能。部分评估包括内联代码、消除虚拟调度并用直接调用替换它们,以及构建一个将被提交给 Graal 编译器进行进一步优化的组合单元。Truffle 在可能被证伪的假设处放置守卫点。这些守卫点通过使代码无效并切换回执行解释器模式来触发降级优化。然后,在进行了激进的常量折叠、内联和逃逸分析之后,将代码编译为客语言的目标机器码。Truffle 在 AST 上执行内联,使其与语言无关。内联决策是通过在每个候选对象上执行部分评估来做出的。
在下一节中,我们将探讨 GraalVM 中的 Truffle 框架,该框架用于创建 DSL。
学习 Truffle DSL
Truffle 定义了一个基于 Java 注解处理器的 领域特定语言(DSL)。语言开发者必须编写大量的样板代码来管理特殊化的状态。为了理解 Truffle DSL 如何使程序员的生活变得容易,让我们快速举一个例子:
c = a + b
如我们本章前面所讨论的,在 AST 中,每个操作和操作数都表示为一个节点。在 Truffle 中,它是一个从 com.oracle.truffle.api.nodes.Node 派生的 Java 类。为了理解 DSL 的必要性,让我们对前面表达式的 AST 实现进行过度简化。
由于我们正在查看动态类型语言,a 和 b 可以是任何类型。我们需要一个表达式节点,该节点应该实现一个 execute 方法,用于检查 a 和 b 的所有可能类型。我们可能需要编写类似于以下逻辑的代码:

图 6.5 – 实现特殊化的守卫检查 – 流程图
在前面的流程图中,我们正在检查所有可能的操作数类型组合,并根据这些组合评估表达式,如果这些条件都不满足,则抛出 TypeError 异常。这种逻辑需要在 Truffle 解释器中编写,因为我们正在处理动态类型语言。
如果我们将此转换为 Truffle 解释器代码,这将是一个非常简单的表达式的大量代码。想象一下,如果我们有更复杂的表达式和其他操作和函数。Truffle 解释器代码将难以编写和管理。
这就是 Truffle DSL 解决问题的所在。Truffle DSL 提供了一个非常明确的节点层次结构、注解和注解处理程序的框架,可以用来处理这种类型的动态性。
@Specialization 注解是由 com.oracle.truffle.api.dsl.Specialization 类实现的特殊化注解,它被用作所有可能的评估情况的注解(如图中之前的绿色框所示)。Truffle DSL 将其编译成动态代码,其中 Truffle 根据操作数参数选择正确的实现(序列中的第一个)。语言开发者的代码将类似于以下代码片段:
@Specialization protected long executeAddInt (int left, int right) {
return left + right;
}
@Specialization String executeAddFloat (Float left, Float right) {
return left + right;
}
@Specialization String executeAddString (String left, String right) {
return left + right;
}
前面的代码展示了 Truffle DSL 如何简化工作,我们不需要编写大量的 if/else 语句。Truffle DSL 注解会为我们编译和生成这些代码。最后,为了处理异常情况,我们可以使用由 com.oracle.truffle.api.dsl.Fallback 类实现的 @Fallback 注解。回退代码块将类似于以下代码片段:
@Fallback protected void typeError (Object left, Object right) {
throw new TypeException("type error: args must be two integers or floats or two", this);
}
如前所述,Truffle 根据操作数类型动态选择正确的实现,默认情况下。但是,也可以通过使用 @Specilization 注解声明守卫来修改这一点。可以为 @Specialization 注解声明四种类型的守卫。它们如下:
-
在
Node类声明中的@NodeChild类型匹配时,将执行特定的方法。 -
@Specification注解。这些表达式非常简单,类似于 Java 代码,其结果为布尔值。如果这个表达式评估为真,那么将执行特定的方法;如果为假,解释器将跳过该执行。以下是一个简单的例子:@Specialization(guards = {"!isInteger(operand)", "!isFloat(operand)"}) protected final int executeTheMethod(final Object operand) { //....code to execute if the expression is true }在前面的代码中,如果传递给
guards的表达式为true,Truffle 解释器会选择executeTheMethod()方法。在这种情况下,如果操作数既不是整数也不是浮点数,则该表达式为真。guards实际上是com.oracle.truffle.api.dsl.Specialization中的一个 String 数组属性。我们可以传递多个表达式。 -
ArithmeticException。我们可以有多个特殊化实现来重写执行以处理异常情况。为了更好地理解这一点,让我们看看以下代码示例:@Specialization(rewriteOn = ArithmeticException.class) int executeNoOverflow(int a, int b) { return Math.addExact(a, b); } @Specialization long executeWithOverflow(int a, int b) { return a + b; }在此代码中,当整数类型匹配(类型守卫)时,Truffle 将调用
executeWithOverflow()方法,但如果整数值导致溢出,则会抛出ArithmeticException。在这种情况下,Truffle 将使用executeNoOverflow()方法来覆盖加法方法。这是我们之前在本章中讨论过的基于特殊化的节点重写的一个例子。 -
Assumption对象由 Truffle 用于验证和无效化一个假设,其中com.oracle.truffle.api.Assumption是一个接口。一旦假设被无效化,在该运行时中它将永远不再有效。这被 Truffle 用于在优化和去优化中做出决策。它就像一个全局的布尔标志。语言开发者可以通过程序方式无效化一个假设,让 Truffle 运行时知道特定的假设不再有效,相应地,Truffle 运行时可以做出决策。假设对象通常作为节点的最终字段存储。假设守卫用于在假设为真时选择特殊化方法。
基于这些各种注解,Truffle 将生成实际的execute()方法,其中包含所有if/else控制,以确保根据我们使用@Specification注解声明的约束调用方法的确切版本。Truffle DSL 注解生成器还在execute()方法的末尾包括了CompilerDirectives.transferToInterpreterAndInvalidate();这将告诉编译器停止编译,插入一个转换到解释器的操作,并使机器代码无效。这将触发去优化并返回到执行的解释器模式。
除了这个之外,Truffle DSL 还提供了其他使语言开发者工作变得容易的注解。您可以在此处查看完整列表:www.graalvm.org/truffle/javadoc/com/oracle/truffle/api/dsl/package-summary.html。
Truffle 定义了一个 TypeSystem,语言开发者可以在其中为操作数类型提供自定义的转换行为。例如,Truffle 解释器可能不知道如何将long类型转换为int类型。使用 TypeSystem,我们可以定义类型转换逻辑。在特殊化过程中,Truffle 解释器将使用 TypeSystem。
动态类型语言的一个挑战是多态方法/函数的调度。Truffle 解释器实现多态内联缓存以加快函数查找。
多态内联缓存
在动态类型语言中,解释器必须执行查找以确定被调用方法/函数的正确实现。查找函数和调用函数是昂贵的,并且会减慢执行速度。在动态类型语言中,当调用一个对象或函数时,类在构建时或运行时都没有声明,解释器必须执行查找以找到实际实现该方法的类。这通常是一个散列表查找,与强类型语言中发生的 vTable 查找不同。散列表查找耗时且非常昂贵,会减慢执行速度。如果我们只有一个类实现该方法,我们只需要执行一次查找。这被称为单态内联。如果有多个类实现该方法,则是多态的。
检查函数查找是否有效比实际查找更便宜。如果对于多个(多态)函数的多次查找有很多之前的查找,Truffle 会缓存多态查找。当函数由于去优化而重新定义时,使用Assumption对象来无效化并执行新的查找。为了提高查找性能,Truffle 提供了多态内联缓存。Truffle 缓存查找,并仅检查查找是否仍然有效。
理解 Truffle 如何支持互操作性
Truffle 提供了一个非常精心设计的互操作性框架,允许客语言读取和存储数据。在本节中,我们将介绍 Truffle 互操作性框架提供的一些关键特性。让我们逐一了解它们。
框架管理和局部变量
Truffle 提供了一个标准接口来处理主机和客语言实现之间的局部变量和数据。框架提供了读取和存储当前命名空间中数据的接口。当调用函数时,局部变量的数据作为com.oracle.truffle.api.frame.Frame实例传递。框架有两种实现:
-
execute()方法。这是轻量级的,并且更可取,因为 Graal 可以更好地优化它。这个框架存在于函数的作用域内。这是传递数据给函数的最优和推荐方式。VirtualFrame不会逃逸,因此易于处理和内联。 -
MaterializedFrame在堆中分配,并且可以被其他函数访问。MaterializedFrame超出函数的作用域。Graal 无法像优化VirtualFrame那样优化它。这种框架实现也对内存和速度有影响。
帧跟踪作为键的一部分存储的数据类型。用于获取数据的键是 FrameSlot 和 FrameSlotKind 的实例。以下代码片段显示了 Frame 接口的定义:
public interface Frame {
FrameDescriptor getFrameDescriptor();
Object[] getArguments();
boolean isType(FrameSlot slot);
Type getType(FrameSlot slot) throws FrameSlotTypeException;
void setType(FrameSlot slot, Type value);
Object getValue(FrameSlot slot);
MaterializedFrame materialize();
}
FrameSlot.getIdentifier() 提供了数据的唯一标识符,而 FrameSlotKind 存储数据的类型。FrameSlotKind 是一个包含各种类型(布尔型、字节型、双精度型、浮点型、非法型、整型、长整型、对象型)的枚举。
FrameDescriptor 类跟踪存储在帧中的值。FrameDiscriptor 描述了 Frame 的布局,提供了 FrameSlot 和 FrameSlotKind 以及值的映射。请参阅 www.graalvm.org/truffle/javadoc/com/oracle/truffle/api/frame/package-summary.html 了解 Frame API 的更多详细信息。SimpleLanguage 有一个帧管理的实现,是理解如何使用 Frame API 来管理在调用方法/函数时在不同语言之间传递的数据的好起点。
动态对象模型
Truffle 提供了一个 动态对象模型 (DOM),它提供了一个对象存储框架,以实现不同语言之间数据和对象的可互操作性。Truffle 的 DOM 定义了一种标准且优化的数据共享方式,尤其是在动态类型语言之间。DOM 提供了一个语言无关的共享基础设施,允许开发者派生和实现各种语言的对象实现。这也帮助我们实现不同语言之间的类型对象共享。Truffle 的 DOM 是 Truffle 互操作性和嵌入功能的核心组件之一。它为主机和客户端语言提供了一致的内存对象存储结构。这允许不同语言编写的代码之间共享数据,并在多语言应用程序中应用优化。
动态类型语言的一个挑战是期望数据对象模型具有动态性。对象的结构可能会动态改变。为了支持这一点,Truffle 的 DOM 定义了一个名为 DynamicObject 的 Java 类。它提供了扩展数组以提供原始类型和对象扩展的可变性。
客户端语言对象都应该继承自一个从 DynamicObject 扩展并实现 TruffleObject 的基类。现在让我们详细了解 Truffle 仪表化。
理解 Truffle 仪表化
Truffle 提供了一个 Instrumentation API,以帮助构建用于诊断、监控和调试的仪器和工具。Truffle 还提供了一个名为 Simple Tool 的参考实现(github.com/graalvm/simpletool)。Truffle 提供了一个非常高性能的仪器化设计。仪器化是通过探针和标签实现的。探针附加到 AST 节点以捕获仪器化数据,节点使用标签进行标识。多个仪器可以附加到探针。以下图示了一个典型的仪器化:

图 6.6 – Truffle 仪器化
前面的图示说明了 Truffle 的 Instrument API 如何连接到 AST 以收集各种指标/数据。Truffle 通过插入包装节点替换原始节点,并将信息传递给探针节点,该节点可以连接到多个仪器以收集数据。
使用 Truffle 进行 AOT 编译
客户端语言开发者可以通过返回非空值来使用 Graal 的 RootNode.prepareForAOT() 方法。如果返回空值,Truffle/Graal 会理解该语言不支持构建原生镜像。为了支持 AOT,prepraeForAOT() 方法通常可能实现以下任务:
-
提供局部变量的类型信息并在
FrameDescriptor中更新它们。这将有助于 AOT 编译器在构建时解析类型。 -
解决并定义参数和返回类型。
Truffle DSL 提供了辅助类以加速 AOT 功能的开发。com.oracle.truffle.api.dsl.AOTSupport 类递归地准备 AST 以进行 AOT。AST 中的每个节点都必须实现 prepareForAOT() 方法。
可以通过将 --engine.CompileAOTOnCreate=true 参数传递给语言启动器来触发 AOT 编译。每个客户端语言都将有一个语言启动器来运行应用程序,例如,js 用于 JavaScript,graalpython 用于 Python 等。我们将在下一章中介绍一些这些客户端语言实现。
使用启动器选项优化 Truffle 解释器性能
Truffle 定义了一个设计和规范,用于提供各种启动器选项,这些选项可用于诊断、调试和优化解释器。所有客户端语言开发者都支持这些启动器选项。在本节中,我们将介绍一些这些重要的启动器选项:
-
-help命令行参数。--help:expert提供专家选项。对于语言实现者的内部选项,我们可以使用--help:internal。 -
--vm.Dgraal.Dump=Truffle:1参数。使用 Truffle 生成的 Graal 图将包含一个名为 After TruffleTier 的阶段,该阶段显示了 Truffle 执行的优化。--cpusampler可以用来找出应用程序运行的 CPU 时间,并提供按模块详细分解的 CPU 使用情况。可以传递
--engine.TraceCompilation参数来在每次方法编译时创建一个跟踪。可以传递
--engine.TraceCompilationDetail参数来跟踪编译队列、开始和完成时的编译过程。可以传递
--engine.TraceCompilationAST参数来跟踪代码编译时的 AST。可以传递
--engine.TraceInlining参数来跟踪客语言所做的内联决策。可以传递
--engine.TraceSplitting参数来跟踪语言所做的拆分决策。可以传递
--engine.TraceTransferToInterpreter参数来跟踪在触发去优化并发生到解释器的转换时的情况。
您可以参考 GraalVM 文档以获取更多信息 (www.graalvm.org/graalvm-as-a-platform/language-implementation-framework/Optimizing/) 或在语言启动器中传递 --help 参数。
SimpleLanguage 和 Simple Tool
GraalVM 团队创建了一个名为 SimpleLanguage 的客语言参考实现。SimpleLanguage 展示了 Truffle 的功能,并解释了如何使用 Truffle API。客语言开发者可以使用 SimpleLanguage 作为参考。它是完全开源的,可在 GitHub 上找到 github.com/graalvm/simplelanguage。SimpleLanguage 只是一个起点,并不实现所有功能。
还有一个 Simple Tool 的参考实现。Simple Tool 是一个使用 Truffle 构建的代码覆盖率工具的实现。这也是一个开源项目,工具开发者可以使用它来构建在 GraalVM 上运行的新的工具。您可以在 github.com/graalvm/simpletool 访问此工具的源代码。
使用 Truffle 开发的语言数量正在不断增加。在接下来的两章中,我们将介绍 JavaScript、LLVM(C/C++)、Ruby、Python、R、Java/Truffle 和 WebAssembly。您可以在 www.graalvm.org/graalvm-as-a-platform/language-implementation-framework/Languages/ 查看一些其他编程语言的状态。
摘要
在本章中,我们探讨了 Truffle 的架构,并了解了它是如何为其他语言(客语言)在 GraalVM 上运行提供精心设计的框架。我们还研究了 Truffle 解释器的实现方式以及它们如何在提交稳定后的 AST 到 Graal 进行进一步优化之前对 AST 进行优化。
在本章中,您已经对 Truffle 架构以及 Truffle 如何在 Graal 之上提供框架和实现层有了很好的理解。您还探索了 Truffle 在将代码提交给 Graal JIT 进行进一步优化和执行之前所执行的优化。
在下一章中,我们将探讨 JavaScript 和 LLVM 语言(C、C++等)如何在 GraalVM 上实现 Truffle 并运行。
问题
-
什么是特化?
-
什么是树/节点重写?
-
什么是部分评估?
-
什么是 Truffle DSL?
-
什么是框架?
-
什么是动态对象模型?
进一步阅读
-
Truffle:一个自优化运行时系统 (
lafo.ssw.uni-linz.ac.at/pub/papers/2012_SPLASH_Truffle.pdf) -
特化动态技术以实现 Ruby 编程语言 (
www.researchgate.net/publication/285051808_Specialising_Dynamic_Techniques_For_Implementing_the_Ruby_Programming_Language) -
用于构建自优化 AST 解释器的领域特定语言 (
lafo.ssw.uni-linz.ac.at/papers/2014_GPCE_TruffleDSL.pdf) -
一个虚拟机统治一切 (
lafo.ssw.uni-linz.ac.at/papers/2013_Onward_OneVMToRuleThemAll.pdf) -
多语言运行时中的高性能跨语言互操作性 (
chrisseaton.com/rubytruffle/dls15-interop/dls15-interop.pdf) -
在 Truffle 中编写语言 (
cesquivias.github.io/index.html)
第九章:GraalVM 多语言 – JavaScript 和 Node.js
在上一章中,我们探讨了 Truffle 如何提供一层来集成其他语言程序以便在 GraalVM 上运行。在本章中,我们将重点关注 JavaScript 和 Node.js 解释器,在下一章中,我们将介绍其他运行时,例如 Java/Truffle、Python、R 和 WebAssembly。我们将探讨 Truffle 的多语言互操作性功能,并探索 JavaScript 解释器。我们将通过编写代码来亲身体验这些功能。
在本章中,我们将涵盖以下主题:
-
理解如何在 Graal 上运行非 JVM 语言应用程序,特别是 JavaScript 和 Node
-
学习如何在用不同语言编写的应用程序之间传递对象/值
-
理解如何使用优化技术来微调代码
到本章结束时,您将非常清楚地了解如何在 GraalVM 上构建多语言应用程序。
技术要求
在本章中,我们将进行大量的动手编码,以探索 GraalVM 支持的各种客语言。要尝试代码,您需要以下内容:
-
各种语言的 Graal 运行时:我们将在本章中介绍如何安装和运行这些运行时。
-
访问 GitHub:有一些示例代码片段,这些代码片段存储在 Git 仓库中。您可以从以下链接下载代码:
github.com/PacktPublishing/Supercharge-Your-Applications-with-GraalVM/tree/main/Chapter07/js。您将在chapter7目录下找到特定章节的代码。 -
本章的“代码实战”视频可以在
bit.ly/3yqu4ui找到。
理解 JavaScript(包括 Node.js)Truffle 解释器
GraalVM 的 JavaScript 版本是一个符合 ECMAScript 标准的运行时,适用于 JavaScript (js) 和 Node.js (node)。它支持截至本书编写时的 ECMAScript 2021 的所有功能。它还与 Nashorn 和 Rhino 兼容,并提供对 Node.js 的完全支持。
GraalVM Node.js 使用原始的 Node.js 源代码,并用 GraalVM JavaScript 引擎替换了 V8 JavaScript 引擎。这种替换是无缝的,应用程序开发者不需要修改大量代码或配置,就可以使用 GraalVM Node.js 运行现有的 Node.js 应用程序。GraalVM Node.js 为嵌入来自其他语言的代码、访问数据和代码以及与其他语言代码的互操作提供了更多功能。npm。
在本节中,除了将 JavaScript 和 Node 作为运行应用程序的替代运行时之外,我们还将探索它们的互操作性功能。我们将通过大量的 JavaScript 和 Node.js 示例代码来探索 GraalVM JavaScript 运行时的多语言能力。
验证 JavaScript、Node 和 npm 的安装和版本
JavaScript 和 Node.js 随 GraalVM 安装一起提供;你可以在 <GraalHome>/bin 目录中找到它们。我们可以通过检查版本号来验证 JavaScript 运行时是否配置正确。
要检查版本,请执行 js --version 命令。在撰写本书时,GraalVM JavaScript 21.0.0.2 是最新的。以下是输出(注意,这是 GraalVM JavaScript):
GraalVM JavaScript (GraalVM EE Native 21.0.0.2)
我们还可以通过执行 node --version 命令来确保我们正在运行正确的 Node.js 版本。在以下命令中,我们明确调用正确的版本。请注意,对于您来说,GraalVM 主目录可能不同:
/Library/Java/JavaVirtualMachines/graalvm-ee-java11-21.0.0.2/Contents/Home/bin/node --version
v12.20.1
让我们也通过执行 npm --version 命令来确保 NPM 正在运行。以下是命令和输出:
/Library/Java/JavaVirtualMachines/graalvm-ee-java11-21.0.0.2/Contents/Home/bin/npm --version
6.14.10
既然我们已经验证了 JavaScript、Node.js 和 npm 的安装,让我们创建一个简单的 Node.js 应用程序。
进入应用程序文件夹,并执行 npm init。这将设置 Node.js 应用程序的样板配置。我们将应用程序命名为 graal-node-app。以下显示的是控制台输出:
/Library/Java/JavaVirtualMachines/graalvm-ee-java11-21.0.0.2/Contents/Home/bin/npm init
package name: (npm) graal-node-app
version: (1.0.0) 1.0.0
description:
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)
About to write to /chapter7/js/npm/package.json:
{
"name": "graal-node-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" andand exit 1"
},
"author": "",
"license": "ISC"
}
Is this OK? (yes)
这将创建一个基于我们选择的选项的 Node.js 应用程序和 package.json 文件,其中包含样板配置。让我们通过执行 npm install --save express 来安装 express 包。这将把 express 包安装到应用程序文件夹中,并更新 package.json 文件(因为使用了 --save 参数)。以下是输出:
/Library/Java/JavaVirtualMachines/graalvm-ee-java11-21.0.0.2/Contents/Home/bin/npm install --save express
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN graal-node-app@1.0.0 No description
npm WARN graal-node-app@1.0.0 No repository field.
+ express@4.17.1
added 50 packages from 37 contributors and audited 50 packages in 6.277s
found 0 vulnerabilities
你将找到包含运行我们的应用程序所需的所有包的 node_modules 目录。让我们创建一个 index.js 文件,并使用以下代码:
var express = require('express');
var app = express();
app.get('/', function(request, response) {
var responseString = "<h1>Hello Graal Node </h1>";
response.send(responseString);
});
app.listen(8080, function() {
console.log('Started the server at 8080')
});
如您所见,这是一个非常简单的应用程序,当在根目录下调用时,会以 HTML Hello Graal Node 作为标题 1 响应。应用程序将在端口号 8080 上监听。
使用以下命令运行此应用程序:
/Library/Java/JavaVirtualMachines/graalvm-ee-java11-21.0.0.2/Contents/Home/bin/node index.js
Started the server at 8080
既然我们可以看到输出,我们知道应用程序正在监听 8080。让我们尝试从 Web 浏览器在 http://localhost:8080/ 上调用它。以下是应用程序在 Web 浏览器上响应的截图:

图 7.1 – Hello Graal Node 截图
既然我们知道 GraalVM 上的 Node.js 运行正常,让我们了解多语言互操作性。
JavaScript 互操作性
在 第六章,“Truffle – 概述”中,我们详细介绍了 Truffle 如何启用多语言支持,并提供多语言互操作性和嵌入的基础设施。在本节中,我们将通过示例代码探索这些功能。
让我们取用上一节中创建的 Node.js 应用程序,并在我们的index.js文件中添加一个端点/poly。让我们创建一个简单的 Python 数组对象,并在对象中存储一些数字。然后我们将遍历这个 Python 对象在 Node.js 中,列出这些数字。这展示了我们如何在 JavaScript 中嵌入 Python 代码片段。
以下源代码显示了这个新的端点/poly:
var express = require('express');
var app = express();
app.get('/', function(request, response) {
var responseString = "<h1>Hello Graal Node </h1>";
response.send(responseString);
});
app.get('/poly', function(request, response) {
var responseString = "<h1>Hello Graal Polyglot </h1>";
var array = Polyglot.eval("python", "[1,2,3,4, 100, 200, 300, 400]")
responseString = responseString + "<ul>";
for (let index = 0; index < array.length; index++) {
responseString = responseString + "<li>";
responseString = responseString + array[index];
responseString = responseString + "</li>";
}
responseString = responseString + "</ul>";
response.send(responseString);
});
现在,让我们编写代码来监听8080端口,并在收到请求时调用前面的函数:
app.listen(8080, function() {
console.log('Started the server at 8080')
});
如您在代码中所见,我们正在使用Polyglot.eval()方法来运行 Python 代码。为了让 polyglot 对象知道它是 Python 代码,我们传递了python作为参数,并传递了数组的 Python 表示。现在让我们用node运行此代码:
/Library/Java/JavaVirtualMachines/graalvm-ee-java11-21.0.0.2/Contents/Home/bin/node --jvm --polyglot index.js
Started the server at 8080
注意,我们必须向 node 传递--jvm和--polyglot参数。传递这些参数非常重要。--jvm告诉 node 在--polyglot上运行,正如其名称所暗示的,告诉 node 支持polyglot。由于 Truffle 和 Graal 在 JVM 上运行,因此使用jvm参数很重要,即使我们可能没有在我们的代码中直接使用 Java。
现在,让我们从浏览器中访问这个新的端点。以下截图显示了预期的输出:

图 7.2 – /poly端点结果截图
如您可能已注意到,我们第一次调用时页面加载需要时间,但随后的调用都是瞬间的。让我们用 curl 来计时(curl 是一个用于调用任何 URL 的命令行工具。有关 curl 的更多详细信息以及如何在您的机器上安装 curl,请参阅curl.se/)。以下是一系列 curl 命令的截图:

图 7.3 – 随后调用后 Node.js 的性能
我们可以看到 CPU 的初始负载,但随后的调用快速进行,没有额外的 CPU 负载。
现在,让我们探索 polyglot 互操作性的更多高级功能。JavaScript 和 Java 互操作性非常复杂。让我们通过比列表更复杂的实现来探索这些概念。
Java 中的 JavaScript 嵌入代码
让我们回顾一下在前面章节中使用的FibonaaciCalculator.java文件。让我们将FibonacciCalculator.java修改为使用 JavaScript 片段并在 Java 中执行该 JavaScript 片段。
这里是带有嵌入 JavaScript 片段的修改后的FibonacciCalculator版本。Java 文件名为FibonacciCalculatorPolyglot.java。您可以在 Git 仓库中找到完整的代码:
import org.graalvm.polyglot.*;
import org.graalvm.polyglot.proxy.*;
我们必须导入polyglot类。这实现了 Truffle 互操作性:
public class FibonacciCalculatorPolyglot{
static String JS_SNIPPET = "(function logTotalTime(param){console.log('total(from JS) : '+param);})";
public int[] findFibonacci(int count) {
int fib1 = 0;
int fib2 = 1;
int currentFib, index;
int [] fibNumbersArray = new int[count];
for(index=2; index < count; ++index ) {
currentFib = fib1 + fib2;
fib1 = fib2;
fib2 = currentFib;
fibNumbersArray[index - 1] = currentFib;
}
return fibNumbersArray;
}
现在,让我们定义main()函数,它将多次调用findFibonacci()以达到编译器阈值:
public static void main(String args[]){
FibonacciCalculatorPolyglot fibCal = new FibonacciCalculatorPolyglot();
long startTime = System.currentTimeMillis();
long now = 0;
long last = startTime;
for (int i = 1000000000; i < 1000000010; i++) {
int[] fibs = fibCal.findFibonacci(i);
long total = 0;
for (int j=0; j<fibs.length; j++) {
total += fibs[j];
}
now = System.currentTimeMillis();
System.out.printf("%d (%d ms)%n", i , now – last);
last = now;
}
long endTime = System.currentTimeMillis();
long totalTime = System.currentTimeMillis() - startTime;
System.out.printf("total (from Java): (%d ms)%n", totalTime);
try (Context context = Context.create()) {
Value value = context.eval("js", JS_SNIPPET);
value.execute(totalTime);
}
}
}
让我们探索这段代码。我们定义了一个静态String变量,它包含下一个显示的 JavaScript 代码片段:
static String JS_SNIPPET = "(function logTotalTime(param){console.log('total(from JS) : '+param);})";
我们定义了一个静态String,其中包含一个简单的 JavaScript 函数,该函数打印传递给它的任何参数。要在 Java 中调用此 JavaScript 代码,我们需要首先通过导入以下包来导入Polyglot库:
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
要调用 JavaScript 代码,我们首先需要创建org.graalvm.polyglot.Context类的实例。Context对象提供了多语言上下文,允许客语言代码在宿主语言中运行。多语言上下文表示所有已安装和允许的语言的全局运行时状态。
使用Context对象的最简单方法是创建Context对象,并在Context对象中使用eval()函数执行其他语言代码。以下是一个代码片段,其中我们在 Java 中执行 JavaScript 代码片段。在这种情况下,客语言是 JavaScript,它作为参数"js"传递到宿主语言 Java 的eval方法中:
try (Context context = Context.create()) {
Value value = context.eval("js", JS_SNIPPET);
value.execute(totalTime);
}
现在让我们执行此代码。以下是执行后的输出截图:

图 7.4 – FibonacciCalculatorPolyglot 的输出,显示 Java 和 JavaScript 的输出
如您在输出中看到的,我们打印了两个总数,一个是用 Java 代码打印的,另一个是从 JavaScript 代码打印的。
这打开了许多可能性——想象一下在 Node.js 网络应用程序中运行用 Python 或 R 编写的机器学习代码。我们正在将单个语言的最佳特性结合在一个虚拟机中。
Context对象有ContextBuilder,可以用来设置特定的环境属性。以下是可以设置的属性之一,以及相关的Context创建代码。这可以用来控制客语言对宿主的访问。控制访问的代码是Context.newBuilder().allowXXX().build()。以下是可以用于更精细访问控制的allowXXX方法的各个版本:
-
allowAllAccess(boolean): 这是默认设置。它为客语言提供所有访问权限。 -
allowCreateProcess(boolean): 为客语言创建新进程提供控制访问。 -
allowCreateThread(boolean): 为客语言创建新线程提供控制访问。 -
allowEnvironmentAccess(EnvironmentAccess): 允许使用提供的策略控制访问环境。 -
allowHostClassLoading(boolean): 这允许客语言通过 JAR 文件或类文件加载新的宿主类。 -
allowIO(boolean): 控制执行 I/O 操作的访问。如果为 true,客语言可以在宿主系统上执行不受限制的 I/O 操作。 -
allowNativeAccess(boolean): 控制客语言访问本地接口。 -
allowPolyglotAccess(PolyglotAccess): 使用提供的策略控制多语言访问。PolyglotAccess可以用来定义自定义的多语言访问策略,以更细粒度地控制数据、绑定和代码执行。这是一个自定义实现,访客语言可以使用PolyglotAccess构建器来构建。
请参阅 Javadoc (www.graalvm.org/truffle/javadoc/org/graalvm/polyglot/Context.html) 以获取有关其他方法的更多详细信息。给访客语言所有访问权限是有风险的;根据需求提供精细和具体的访问总是更好的。
以下是如何使用特定访问构建 Context 对象的示例:
Context context = Context.newBuilder().allowIO(true).build();
我们还可以使用以下代码片段加载外部文件,这是嵌入代码的推荐方式。将其他语言的代码作为字符串复制粘贴到宿主语言中并不是一个好的做法。保持代码更新且无错误是一项配置管理噩梦,因为其他语言的代码可能由不同的开发者开发。
以下是一个代码片段,展示了如何将源代码作为文件加载,而不是将访客语言代码嵌入到宿主源代码中:
Context ctx = Context.newBuilder().allowAllAccess(true).build();
File path = new File("/path/to/scriptfile");
Source pythonScript = Source.newBuilder("python", new File(path, "pythonScript.py")).build();
ctx.eval(pythonScript)
在本节中,我们看到了如何从 Java 中调用 JavaScript 代码。现在让我们尝试从 JavaScript 中调用一个 Java 类。
从 JavaScript/Node.js 调用 Java 类
现在我们已经看到了 Java 代码如何运行 JavaScript 代码,让我们尝试从 JavaScript 中调用 Java 代码。以下是一个非常简单的 Java 应用程序,它在控制台上打印传递给它的参数。Java 文件的名称是 HelloGraalPolyglot.java:
public class HelloGraalPolyglot {
public static void main(String[] args) {
System.out.println(args[0]);
}
}
让我们使用 javac HelloGraalPolyglot.java 编译此应用程序。
现在让我们尝试从 JavaScript 中调用此应用程序。以下是在 JavaScript 中的代码 hellograalpolyglot.js:
var hello = Java.type('HelloGraalPolyglot');
hello.main(["Hello from JavaScript"]);
这是非常简单的 JavaScript 代码。我们使用 JavaScript 中的 Java.type() 方法加载 Java 类,并使用 String 参数调用 main() 方法,并传递字符串 "Hello from JavaScript"。
要执行此 JavaScript,我们必须传递 --jvm 参数和 --vm.cp 来设置类路径。以下是命令:
js --jvm --vm.cp=. hellograalpolyglot.js
以下显示了执行此命令的输出:
js --jvm --vm.cp=. hellograalpolyglot.js
Hello from JavaScript
这是一个非常简单的例子。为了理解参数是如何传递的,以及方法返回的数据是如何在 JavaScript 中捕获和使用,让我们尝试从 Node.js 应用程序中调用定义在 FibonacciCalculator.java 代码中的 findFibonacci() 方法。我们将传递一个参数,并从方法中获取一个数组,我们将将其渲染为网页。
让我们修改 index.js 并添加另一个端点 /fibonacci。以下是完整的源代码:
app.get('/fibonacci', function(request, response) {
var fibonacciCalculatorClass = Java.type("FibonacciCalculatorPolyglot");
var fibonacciCalculatorObject = new fibonacciCalculatorClass();
//fibonacciCalculatorClass.class.static.main([""]);
var array = fibonacciCalculatorObject.findFibonacci(10);
var responseString = "<h1>Hello Graal Polyglot - Fibonacci numbers </h1>";
responseString = responseString + "<ul>";
for (let index = 0; index < array.length; index++) {
responseString = responseString + "<li>";
responseString = responseString + array[index];
responseString = responseString + "</li>";
}
responseString = responseString + "</ul>";
response.send(responseString);
});
在这个node.js代码中,我们首先使用Java.Type()方法加载 Java 类FibonacciCalculatorPolyglot。然后我们创建这个类的实例并直接调用方法。我们知道输出是一个数组。我们正在遍历数组并将结果打印为 HTML 列表。
让我们使用以下命令运行此代码:
/Library/Java/JavaVirtualMachines/graalvm-ee-java11-21.0.0.2/Contents/Home/bin/node --jvm --polyglot index.js
Started the server at 8080
现在,让我们转到 http://localhost:8080/fibonacci。以下是输出截图:

图 7.5 – 调用 FibonacciCalculator 方法的 Node.js 应用程序输出截图
上述截图显示了 Node.js/Fibonacci 端点正在工作,其中它以 HTML 列表的形式列出前 10 个斐波那契数。
在本节中,我们探讨了如何在 Java 中运行 JavaScript 代码片段,从 JavaScript 调用 Java,调用 Java 方法,传递参数,以及从 Node.js 应用程序中获取 Java 方法的返回结果。让我们快速总结一下各种 JavaScript 互操作性功能:
-
当我们想从 JavaScript 调用 Java 代码时,我们需要传递
--jvm参数,并使用--vm.cp设置CLASSPATH来加载正确的类。 -
我们在 Java 中使用多语言
Context对象来运行其他语言代码。有一个特殊的ScriptEngine对象用于在 Java 中运行 JavaScript。Context对象封装了这个对象,并且是推荐的运行方式。 -
我们使用
Java.type()从 JavaScript/Node.js 加载 Java 类。 -
我们可以使用
new关键字来创建类的实例。 -
类型转换由 GraalVM 在 Java 和 JavaScript 之间处理。在可能丢失数据的情况下(例如,从
long转换为int),会抛出TypeError。 -
在调用
Java.type()时,可以通过提供完整的包路径来完成 Java 包的解析。 -
异常处理可以通过在 Java 和 JavaScript 中使用
try{}catch块来实现。GraalVM 负责转换异常。 -
在前面的例子中,我们探讨了 Java 数组如何被 JavaScript 迭代。同样,
Hashmap也可以使用put()和get()方法原生地使用。 -
JavaScript 对象可以通过 Java 代码作为
com.oracle.truffle.api.interop.java.TruffleMap类的实例来访问。
在本节中,我们探讨了如何在 Java 和 JavaScript 之间进行互操作。现在,让我们探索如何构建多语言原生镜像。
多语言原生镜像
Graal 还支持创建多语言应用程序的原生镜像。要创建这个 Java 类原生镜像,我们必须使用--language参数来构建原生镜像。以下是可以传递给native-image(原生镜像构建器)的各种语言标志。在第五章中,Graal 即时编译器和原生镜像,我们详细介绍了原生镜像构建器:
--language:nfi
--language:python
--language:regex
--language:wasm
--language:java
--language:llvm
--language:js
--language:ruby
在我们的例子中,我们必须传递 --language:js 以让 Native Image 构建器知道我们在 Java 代码中使用 JavaScript。因此,我们需要执行以下命令:
native-image --language:js FibonacciCalculatorPolyglot
以下是执行命令后的输出截图:

图 7.6 – 多语言原生图像构建输出截图
Native Image 构建器执行静态代码分析并构建我们多语言应用程序的最佳图像。我们应该能够在目录中找到可执行的 fibonaccicalculatorpolyglot 文件。让我们使用以下命令执行原生图像:
./fibonaccicalculatorpolyglot
以下图显示了运行原生图像时的输出截图:

图 7.7 – 多语言原生图像执行结果截图
(在这个例子中,你可能会发现代码在 JIT 模式下运行得更慢。请参阅 第四章,Graal 即时编译器,了解更多关于为什么会出现这种情况的详细信息。)
绑定
绑定对象在 Java 和 JavaScript 之间充当中间层,以访问两种语言之间的方法、变量和对象。为了了解绑定是如何工作的,让我们编写一个非常简单的 JavaScript 文件,该文件有三个方法 – add()、subtract() 和 multiply()。所有三个方法都访问两个数字并返回一个数字。我们还有一个包含简单字符串的变量。以下是 JavaScript 代码,Math.js:
var helloMathMessage = " Hello Math.js Variable";
function add(a, b) {
return a+b;
}
function subtract(a, b) {
return a-b;
}
function multiply(a, b) {
return a*b;
}
这段 JavaScript 代码非常简单且直接。
现在我们编写一个简单的 Java 类,该类加载此 JavaScript 文件,通过传递整数参数调用方法,并打印 JavaScript 方法返回的结果。此类还可以访问变量 helloMathMessage 并打印它。
让我们分析代码以了解其工作原理。以下是代码,MathJSCaller.java:
import java.io.File;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;
我们正在导入所有实现 Truffle 互操作性的多语言类:
public void runMathJS() {
Context ctx = Context.create("js");
try {
File mathJSFile = new File("./math.js");
ctx.eval(Source.newBuilder("js", mathJSFile).build());
在前面的代码中,我们创建 Context 对象,加载 JavaScript 文件并构建它。一旦 JavaScript 被加载,为了从 JavaScript 文件中访问方法成员和变量成员,我们使用 Context.getBindings()。绑定提供了一层,允许多语言访问数据和成员方法:
Value addFunction = ctx.getBindings("js").getMember("add");
Value subtractFunction = ctx.getBindings("js").getMember("subtract");
Value multiplyFunction = ctx.getBindings("js").getMember("multiply");
Value helloMathMessage = ctx.getBindings("js").getMember("helloMathMessage");
System.out.println("Binding Keys :" + ctx.getBindings("js").getMemberKeys());
我们只是打印绑定键以查看所有公开的成员。现在,让我们通过调用方法和访问变量来访问成员:
Integer addResult = addFunction.execute(30, 20).asInt();
Integer subtractResult = subtractFunction.execute(30, 20).asInt();
Integer multiplyResult = multiplyFunction.execute(30, 20).asInt();
System.out.println(("Add Result "+ addResult+ " Subtract Result "+ subtractResult+ " Multiply Result "+ multiplyResult));
System.out.println("helloMathMessage : " + helloMathMessage.toString());
}
最后,我们打印出所有结果。完整的源代码可在 技术要求 部分的 Git 仓库链接中找到。
现在,让我们运行这个应用程序。以下截图显示了输出:

图 7.8 MathJSCaller 执行结果
我们可以看到我们的程序正在运行。它可以加载 JavaScript 的math.js文件并调用所有方法。我们还看到了绑定键的列表,这是通过调用System.out.println("Binding Keys :" + ctx.getBindings("js").getMemberKeys());打印出来的。我们可以看到列表有四个键,它们与math.js文件中的内容相匹配。
在这个例子中,我们看到了绑定对象如何作为接口从 Java 访问 JavaScript 成员。
多线程
GraalVM 上的 JavaScript 支持多线程。在本节中,我们将探讨在多语言环境中 Java 和 JavaScript 之间支持的多种模式。
在线程中创建的 JavaScript 对象只能在该线程中使用,不能从另一个线程访问。例如,在我们的例子中,Value对象如addFunction、subtractFunction等只能与该线程一起使用。
让我们修改MathJSCaller类的runMathJS()方法,使其无限期地运行,以模拟并发访问情况。让我们修改前面的代码,并在单独的线程中调用成员函数。以下是代码片段:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
Integer addResult = addFunction.execute(30, 20).asInt();
Integer subtractResult = subtractFunction.execute(30, 20).asInt();
Integer multiplyResult = multiplyFunction.execute(30, 20).asInt();
}
}
});
thread.start();
我们在单独的线程中复制了成员方法的访问。现在让我们在循环中调用它,以模拟并发访问,使用线程内部的相同Context对象和线程外部的对象。以下代码片段显示了使用相同Context对象在线程外部的调用:
while (true) {
Integer addResult = addFunction.execute(30, 20).asInt();
Integer subtractResult = subtractFunction.execute(30, 20).asInt();
Integer multiplyResult = multiplyFunction.execute(30, 20).asInt();
}
} catch (Exception e) {
System.out.println("Exception : " );
e.printStackTrace();
}
}
当我们运行此代码时,在某个时刻,当两个线程同时访问对象时,我们应该得到以下异常:
$ java MathJSCallerThreaded (docker-desktop/bozo-book-library-dev)
Binding Keys :[helloMathMessage, add, subtract, multiply]
java.lang.IllegalStateException: Multi threaded access requested by thread Thread[Thread-3,5,main] but is not allowed for language(s) js.
…..
为了克服这个问题,建议使用隔离的运行时。我们可以为每个线程创建单独的Context对象,并创建这些对象的新实例,并在该线程中使用它们。以下是修复后的代码:
public void runMathJS() {
Context ctx = Context.create("js");
try {
File mathJSFile = new File("./math.js");
ctx.eval(Source.newBuilder ("js", mathJSFile).build());
Value addFunction = ctx.getBindings("js").getMember("add");
Value subtractFunction = ctx.getBindings("js").getMember("subtract");
Value multiplyFunction = ctx.getBindings("js").getMember("multiply");
Value helloMathMessage = ctx.getBindings("js") .getMember("helloMathMessage");
System.out.println("Binding Keys :" + ctx. getBindings("js").getMemberKeys());
while (true) {
Integer addResult = addFunction.execute(30, 20).asInt();
Integer subtractResult = subtractFunction.execute(30, 20).asInt();
Integer multiplyResult = multiplyFunction.execute(30, 20).asInt();
}
现在,在线程内部,我们创建了一个单独的Context对象。以下代码片段显示了更新的代码:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Context ctx = Context.create("js");
ctx.eval(Source.newBuilder("js", mathJSFile).build());
Value addFunction = ctx.getBindings("js").getMember("add");
Value subtractFunction = ctx.getBindings("js").getMember("subtract");
Value multiplyFunction = ctx.getBindings("js").getMember("multiply");
Value helloMathMessage = ctx.getBindings("js") .getMember("helloMathMessage");
while (true) {
Integer addResult = addFunction.execute(30, 20).asInt();
Integer subtractResult = subtractFunction.execute(30, 20).asInt();
Integer multiplyResult = multiplyFunction.execute(30, 20).asInt();
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
thread.start();
如我们所见,在这段代码中,我们在线程内部创建了一个单独的context对象,它是线程本地的。这不会创建异常。
解决这个问题的另一个方法是,在适当的synchronized块或方法中访问context对象,这样就不会同时访问运行时。以下是带有synchronized块的更新代码:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
// Solution 2
while (true) {
synchronized(ctx) {
Integer addResult = addFunction.execute(30, 20).asInt();
Integer subtractResult = subtractFunction.execute(30, 20).asInt();
Integer multiplyResult = multiplyFunction.execute(30, 20).asInt();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
thread.start();
我们也可以将整个块作为一个synchronized块包含在内,仍然使用相同的Context对象:
while (true) {
synchronized(ctx) {
Integer addResult = addFunction.execute(30, 20).asInt();
Integer subtractResult = subtractFunction.execute(30, 20).asInt();
Integer multiplyResult = multiplyFunction.execute(30, 20).asInt();
}
}
这也将正常运行,但可能比之前的解决方案慢,因为可能会有很多锁在Context对象上。
Java 对象是线程安全的,因此 Java 对象可以在运行在不同线程的 JavaScript 运行时之间访问。
异步编程 - Promise 和 await
异步编程在现代分布式应用中非常突出。JavaScript 使用Promise。Promise对象表示异步活动的完成,以及最终值。Promise对象有三个状态:
-
待定:此状态是初始状态。
-
履行:此状态表示操作成功执行。
-
拒绝:此状态表示操作失败。
有时,我们可能需要 JavaScript 创建一个承诺,逻辑可能运行在 Java 代码中,当 Java 代码完成时,可能需要履行或拒绝这个承诺。为了处理这种情况,Graal 提供了一个PromiseExecuter接口。一个 Java 类必须实现这个接口方法,void onPromiseCreation(Value onResolve, Value onReject);。实现这个接口的 Java 类可以被 JavaScript 用来创建一个Promise对象。JavaScript 可以在实现 void then (Value onResolve, Value onReject);的 Java 对象上调用await,以实现 JavaScript 和 Java 之间的异步编程。
摘要
在本章中,我们详细介绍了 GraalVM/Truffle 为 JavaScript 和 Node.js 提供的各种多语言互操作和嵌入功能。我们通过一些实际的代码示例探讨了所有关键概念,以获得对 JavaScript 和 Node.js 如何调用、传递数据以及与其他语言代码互操作的清晰理解。这是 GraalVM 的一个显著特性。
本章给出的示例将帮助您构建和运行使用 Java 和 JavaScript 语言在同一运行时编写的多语言应用。
在下一章中,我们将继续探索 R、Python 和最新的 Java 在 Truffle 上的应用。
问题
-
用于运行其他语言代码的 JavaScript 对象和方法是什么?
-
Java 中的
Context对象是什么? -
如何控制客户端语言对宿主机的访问?
-
如何构建多语言应用的原生镜像?
-
绑定是什么?
进一步阅读
-
GraalVM 企业版(
docs.oracle.com/en/graalvm/enterprise/19/index.html) -
JavaScript 和 Node.js 参考(
www.graalvm.org/reference-manual/js/) -
Truffle:一个自优化的运行时系统 (
lafo.ssw.uni-linz.ac.at/pub/papers/2012_SPLASH_Truffle.pdf) -
Truffle 语言实现框架的对象存储模型 (
chrisseaton.com/rubytruffle/pppj14-om/pppj14-om.pdf)
第十章: GraalVM 多语言支持 – 在 Truffle、Python 和 R 上运行的 Java
在上一章中,我们介绍了 JavaScript 和 Node.js 解释器以及语言间的互操作性。在本章中,我们将介绍其他语言实现,例如以下内容:
-
Java on Truffle(也称为 Espresso):在 Truffle 上运行的 Java 实现
-
GraalPython:Python 语言解释器实现
-
FastR:R 语言解释器实现
所有这些语言实现目前都处于实验性阶段,因此在撰写本书时并未发布用于生产的版本。然而,我们将探讨其功能和构建一些代码来理解各种概念。
在本章中,我们将涵盖以下主题:
-
理解 Python、R 和 Java/Truffle 解释器
-
了解和探索语言互操作性
-
理解这些各种语言解释器的兼容性和限制
到本章结束时,你将获得使用 Python、R 和 Java/Truffle 解释器构建多语言应用程序的实践经验。
技术要求
本章需要以下内容才能跟随各种编码/实践部分:
-
GraalVM 的最新版本。
-
各种语言的 Graal 运行时。我们将在本章中介绍如何安装和运行这些运行时。
-
访问 GitHub:有一些示例代码片段,可在 Git 仓库中找到。代码可以从以下链接下载:
github.com/PacktPublishing/Supercharge-Your-Applications-with-GraalVM/tree/main/Chapter08。 -
本章的“代码实战”视频可以在
bit.ly/3fj2iIr找到。
理解 Espresso(Java on Truffle)
GraalVM 21.0 是一个重大版本,引入了一个名为 Java on Truffle 的新客机语言运行时。在此之前,我们有选择使用 HotSpot(我们在第二章,JIT、HotSpot 和 GraalJIT)运行 Java,在 Graal JIT(我们在第四章,Graal 即时编译器)上运行,或者使用 Graal AOT(我们在第五章,Graal 预编译器及原生图像)作为原生图像运行 Java。在 GraalVM 21.0 中,Java on Truffle 是新的运行时,可以运行 Java。它的代号为 Espresso。这仍然处于实验性阶段,在撰写本书时并未准备好用于生产。在本节中,我们将了解如何使用这个新的运行时运行 Java 应用程序,以及这如何有助于多语言编程。
Espresso 是 JVM 的简化版本,但实现了 JVM 的所有核心组件,如字节码解释器、字节码验证器、Java 本地接口、Java 调试协议等。Espresso 重新使用了 GraalVM 中的所有类和本地库。Espresso 实现了 libjvm.so API。以下图显示了 Espresso 栈架构:


图 8.1 – Espresso 栈架构
该图显示了 Espresso 在 Truffle 之上的实现方式。
为什么我们需要在 Java 上使用 Java?
在 Truffle(Espresso)上运行 Java 是反直觉的,你可能会想知道在 Graal 上添加额外层运行 Java 的优势。以下是一些运行 Espresso 的优势:
-
热插拔方法、lambda 表达式和运行时/调试时的访问修饰符:Espresso 提供了一种在调试期间运行时热插拔方法、lambda 表达式和访问修饰符的方式。这对于开发者来说是一个非常好的特性,因为它允许他们在调试过程中完全更改代码,而无需停止运行时和重新编译,更改将在运行时生效。这加快了开发者的工作流程并提高了生产力。同时,它还帮助开发者在进行代码提交前进行实验和尝试。
-
运行不受信任的 Java 代码的沙盒:Espresso 在 Truffle 上运行,类似于沙盒,可以运行时具有访问限制。这是一种通过提供特定访问权限来运行不受信任的 Java 代码的绝佳方式。请参阅第七章中的“Java 中嵌入 JavaScript 代码”部分,GraalVM 多语言 - JavaScript 和 Node.js,以了解更多关于如何配置访问限制的信息。
-
使用相同的内存空间在 JVM 和非 JVM 之间实现互操作性:在 Espresso 之前,Java 应用程序和非 JVM 动态客户端语言之间的数据传递不是在相同的内存空间中完成的。这可能是由于性能影响。使用 Espresso,我们可以在相同的内存空间中在 Java 和非 JVM 客户端语言之间传递数据。这提高了应用程序的性能。
-
利用 Truffle 工具和仪器:在 Truffle 上运行 Java 将有助于使用所有使用 Truffle 仪器开发的分析、诊断和调试工具。(请参阅第六章中的“理解 Truffle 仪器”部分,Truffle – 概述。)
-
native-image。 -
运行混合版本的 Java:Espresso 提供了所需的隔离层,以便运行用 Java 8 编写的代码,使其能够在 Java 11 上运行。Java 8 代码可以在 Espresso 上运行,而 Espresso 可能正在运行 GraalVM Java 11。这有助于在不更改代码的情况下运行旧代码,并且可能是谨慎地现代化代码的步骤,而不是我们在从旧版本的 Java 迁移到新版本的 Java 时采用的爆炸式现代化方法。
现在我们来安装和运行简单的 Java 代码在 Espresso 上。
安装和运行 Espresso
Espresso 是一个可选的运行时环境;它必须通过 Graal Updater 工具单独下载和安装。以下是安装 Espresso 的命令:
gu install espresso
为了测试 Espresso 是否已安装,让我们执行一个简单的 HelloEspresso.java 应用程序。这是一个非常简单的 Hello World 程序,它打印一条消息。查看以下 HelloEspresso.java 的代码:
public class HelloEspresso {
public static void main(String[] args) {
System.out.println("Hello Welcome to Espresso!!!");
}
}
让我们使用 javac 编译这个应用程序,并使用以下命令运行它:
javac HelloEspresso.java
要在 Truffle 上运行 Java,我们只需将 -truffle 作为命令行参数传递给 java。运行此命令后,我们应该看到以下输出:
java -truffle HelloEspresso
Hello Welcome to Espresso!!!
这验证了安装。我们还可以使用 -jar 参数与 -truffle 一起运行 JAR 文件。现在让我们探索 Espresso 的多语言功能。
探索 Espresso 与其他 Truffle 语言的互操作性
Espresso 是基于 Truffle 构建的,并实现了 Truffle 多语言和互操作性 API。在本节中,我们将探索这些功能。
在我们开始使用 polyglot 功能之前,我们必须安装 Espresso polyglot 功能。要安装 Espresso polyglot 功能,我们可能需要下载 Espresso JAR 文件。您可以在 www.oracle.com/downloads/graalvm-downloads.html 找到最新版本。
以下截图显示了在撰写本书时我们需要下载的 JAR 文件:
![图 8.2 – Java 在 Truffe JAR 文件下载
![img/Figure_8.2_B16878.jpg]
图 8.2 – Java 在 Truffe JAR 文件下载
下载此文件后,我们可以通过运行以下命令来安装它:
sudo gu install -L espresso-installable-svm-svmee-java11-darwin-amd64-21.0.0.2.jar
安装成功后,我们必须重新构建 libpolyglot 本地镜像,以包含 Espresso 库。这个库是运行 polyglot 支持所必需的:
sudo gu rebuild-images libpolyglot -cp ${GRAALVM_HOME}/lib/graalvm/lib-espresso.jar
这将重新构建 libpolyglot 本地镜像。我们现在可以使用 Espresso 的多语言功能了。让我们在下一节中探索这些功能。
探索 Espresso 与其他 Truffle 语言的互操作性
如你所知,Espresso 实现了 Truffle 实现框架,而 com.oracle.truffle.espresso.polyglot.Polyglot 类实现了 Espresso 中的多语言支持。像任何其他客户端语言一样,我们在命令行参数中使用 -polyglot 来让 Truffle 知道如何创建多语言上下文。Espresso 将一个 Polyglot 对象注入到代码中,可以用来与其他语言进行交互。让我们通过运行以下代码来探索使用 Espresso 的多语言编程:
import com.oracle.truffle.espresso.polyglot.Polyglot;
public class EspressoPolyglot {
public static void main(String[] args) {
try {
Object hello = Polyglot.eval("js", "print('Hello from JS on Espresso');");
} catch (Exception e) {
e.printStackTrace();
}
}
}
让我们理解前面的代码。Polyglot 对象为运行动态语言提供上下文。Polyglot.eval() 方法运行外语言代码。第一个参数表明它是 JavaScript 代码,第二个参数是我们想要执行的实际的 JavaScript 代码。让我们使用以下命令来编译此代码:
javac -cp ${GRAALVM_HOME}/languages/java/lib/polyglot.jar EspressoPolyglot.java
在此命令中,我们明确地将 polyglot.jar 文件传递到 -cp 参数(CLASSPATH)中。polyglot.jar 包含了 Espresso 的所有多语言实现,包括 com.oracle.truffle.espresso.polyglot.Polyglot 的导入。
现在我们将在 Espresso 上运行 Java 应用程序。如果我们想在 Espresso 上运行它,应该传递 -truffle 参数,如果不这样做,它将在主机 JVM 上运行。我们可以看到以下输出:
java -truffle --polyglot EspressoPolyglot
[To redirect Truffle log output to a file use one of the following options:
* '--log.file=<path>' if the option is passed using a guest language launcher.
* '-Dpolyglot.log.file=<path>' if the option is passed using the host Java launcher.
* Configure logging using the polyglot embedding API.]
Hello from JS on Espresso
同样,我们可以调用其他语言代码。Java 是一种强类型语言,与 Truffle 上的其他动态类型语言不同。当我们交换 Espresso(Truffle 上的 Java)和其他动态类型语言(如 JavaScript、Python 等)之间的数据时,我们需要一种方法来转换数据类型。polyglot 对象提供了使用 Polyglot.cast() 方法转换数据的方式。让我们通过以下代码使用一个简单的应用程序来理解如何转换数据:
import com.oracle.truffle.espresso.polyglot.Polyglot;
import com.oracle.truffle.espresso.polyglot.Interop;
导入 Polyglot 和 Interop 类。Polyglot 类帮助我们运行客户端语言,而 Interop 类实现了 Truffle 互操作性 API,它抽象了客户端语言之间的数据类型。Truffle 定义了一个互操作性协议,它对 Truffle 语言、工具和嵌入器之间如何进行数据和消息(方法调用)交换提供了明确的规范:
public class EspressoPolyglotCast {
public static void main(String[] args) {
try {
Object stringObject = Polyglot.eval("js", "'This is a JavaScript String'");
Object integerObject = Polyglot.eval("js", "1000");
Object doubleObject = Polyglot.eval("js", "10.12345");
Object arrayObject = Polyglot.eval("js", "[1234, 10.2233, 'String element',400,500, 'Another Sttring element']");
Object booleanObject = Polyglot.eval("js", "10 > 5");
在前面的代码片段中,我们正在评估各种返回字符串、整数、双精度浮点数、整数数组和一个布尔值的 JavaScript 代码片段。这些值被分配给一个通用的 Object,然后稍后使用 Polyglot.cast() 方法转换为相应的 Java 类型 String、Integer、Double、Integer[] 和 Boolean 对象,如以下代码片段所示:
String localStringObject = Polyglot.cast(String.class, stringObject);
Integer localIntegerObject = Polyglot.cast(Integer.class, integerObject);
Double localDoubleObject = Polyglot.cast(Double.class, doubleObject);
Boolean localBooleanObject = Polyglot.cast(Boolean.class, booleanObject);
System.out.println("\nString Object : " + localStringObject + ", \nInteger : " + localIntegerObject + ", \nDouble : " + localDoubleObject + ", \nBoolean : " + localBooleanObject);
接下来,我们将打印这些值。为了处理数组,让我们使用 Interop 类来获取数组对象的信息,例如使用 Interop.getArraySize() 获取数组的大小,并使用 Interop.readArrayElement() 遍历数组。Interop 还提供了一种检查对象类型并提取特定数据类型值的方法。在我们的例子中,我们评估了一个包含整数、双精度浮点数和字符串对象的 JavaScript 数组。我们将使用 Interop.fitsInInt()、Interop.fitsInDouble() 和 Interop.isString() 方法来检查类型,并相应地使用 Interop.asInt()、Interop.asDouble() 和 Interop.asString() 方法提取值。以下是一个代码片段:
long sizeOfArray = Interop.getArraySize(arrayObject);
System.out.println( "\n Array of objects with Size : " + sizeOfArray );
for (int i=0; i<sizeOfArray; i++) {
Object currentElement = Interop.readArrayElement
(arrayObject, i);
if (Interop.fitsInInt(currentElement)) {
System.out.println("Integer Element: " +Interop.asInt(currentElement));
}
if (Interop.fitsInDouble(currentElement)) {
System.out.println("Double Element: " + Interop.asDouble(currentElement));
}
if (Interop.isString(currentElement)) {
System.out.println("String Element: " + Interop.asString(currentElement));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
这些值随后被打印出来。让我们编译并运行这个应用程序。以下是其输出:
javac -cp ${GRAALVM_HOME}/languages/java/lib/polyglot.jar EspressoPolyglotCast.java
espresso git:(main) java -truffle --polyglot EspressoPolyglotCast
String Object : This is a JavaScript String,
Integer : 1000,
Double : 10.12345,
Boolean : true
Array of objects with Size : 6
Integer Element: 1234
Double Element: 1234.0
Double Element: 10.2233
String Element: String element
Integer Element: 400
Double Element: 400.0
Integer Element: 500
Double Element: 500.0
String Element: Another String element
在输出中,我们可以看到如何将动态类型语言(JavaScript)捕获在通用的 Object 中,然后将其转换为特定类型。我们还可以使用 Polyglot.isForeignObject(<object>) 来检查传递的对象是本地对象还是外部对象。
我们看到了如何从 Espresso 中调用其他 Truffle 语言,就像使用 Context polyglot = Context.newBuilder().allowAllAccess(true).build() 调用其他语言一样,并使用绑定(参考 第七章,GraalVM Polyglot - JavaScript 和 Node.js) 交换数据和调用方法。
Java on Truffle Espresso 目前处于非常早期的版本,并且在撰写本书时处于实验阶段。目前存在许多限制,例如不支持 JVM 工具接口和 Java 管理扩展。在此阶段甚至存在许多性能问题。请参阅 www.graalvm.org/reference-manual/java-on-truffle/ 获取最新更新。
现在让我们看看机器学习中最重要的两种语言 – Python 和 R。
理解 GraalPython – Python Truffle 解释器
GraalVM 提供了一个 Python 运行时环境。Python 运行时环境符合 3.8 版本,并且在撰写本书时仍处于 实验 阶段。在本节中,我们将安装并理解 Python 在 Truffle 和 Graal 上的运行方式。我们还将构建一些示例代码,以了解 Graal Python 的互操作性功能。
安装 Graal Python
Graal Python 是一个可选的运行时环境,并且默认情况下不会与 GraalVM 一起安装。要下载它,您必须使用 Graal Updater 工具。以下命令下载并安装 Graal Python:
gu install python
为了验证安装,让我们运行简单的 Python 代码。以下是 HelloGraalPython.py 的源代码:
print("Hello Graal Python")
这是一个非常简单的 Hello World 应用程序,其中我们正在打印消息。让我们使用 graalpython 运行这个应用程序:
graalpython HelloGraalPython.py
当我们执行前面的命令时,我们应该看到下面的输出:
graalpython HelloGraalPython.py
Hello Graal Python
上述输出显示应用程序正在运行,graalpython正在工作。
graalpython也支持虚拟环境。以下命令将创建一个虚拟环境:
graalpython -m venv <name-of-virtual-env>
此命令将创建一个虚拟环境目录,这将是一个隔离的环境。GraalPython 还附带ginstall工具,用于安装支持的库。以下命令将为graalpython安装numpy。也可以使用pip安装库:
graalpython -m ginstall install numpy
现在让我们了解 GraalPython 的编译和解释器管道是如何工作的。
理解 graalpython 编译和解释器管道
Graalpython 的编译/解释器管道略有不同。为了提高解析性能,解析后,Graalpython 使用一个名为.pyc文件的中间表示形式,这样做是为了加快解析速度。下次我们运行 Python 程序时,Graalpython会查找.pyc文件并验证文件是否存在,以及它是否与 Python 源代码匹配;如果是,它将反序列化该文件以构建 SST 和 ST。否则,它将使用 ANTLR 进行完整解析。以下图显示了完整流程。该图并未捕捉所有细节。请参阅第六章中“探索 Truffle 解释器/编译器管道”部分第六章,Truffle – 概述,以获取关于 Truffle 解释器和 Graal JIT 如何执行代码的更详细解释:

图 8.3 – Graalpython 编译/解释器管道
一旦创建了 SST 和 ST,它们随后将被转换为 AST 中间表示形式并进行优化。在部分评估之后,最终的专用 AST 将被提交给 GraalJIT 进行进一步执行,然后继续常规流程,如第六章中“探索 Truffle 解释器/编译器管道”部分所述第六章,Truffle – 概述。
到目前为止,我们已经学习了如何使用 GraalPython 运行 Python 程序以及 GraalPython 如何使用 Truffle 和 GraalJIT 优化解析和代码优化。现在让我们探索 GraalPython 的多语言互操作性功能。
探索 Java 和 Python 之间的互操作性
在本节中,我们将通过示例 Java 代码探索 Java 和 Python 之间的互操作性。以下代码计算斐波那契数的和。此类有一个findFibonacci()方法,它接受我们需要计算的斐波那契数的数量,并返回这些斐波那契数的数组:
public class FibonacciCalculator{
public int[] findFibonacci(int count) {
int fib1 = 0;
int fib2 = 1;
int currentFib, index;
int [] fibNumbersArray = new int[count];
for(index=2; index < count; ++index ) {
currentFib = fib1 + fib2;
fib1 = fib2;
fib2 = currentFib;
fibNumbersArray[index - 1] = currentFib;
}
return fibNumbersArray;
}
public static void main(String args[]) {
FibonacciCalculator fibCal = new FibonacciCalculator();
int[] fibs = fibCal.findFibonacci(10);
}
}
现在让我们从 Python 代码中调用findFibonacci()方法。以下是从 Java 类返回的数组中调用该方法并迭代的 Python 代码:
import java
import time
fib = java.type("FibonacciCalculator")()
result = fib.findFibonacci(10)
print("Fibonacci number ")
for num in result:
print(num)
在前面的代码中,我们使用 java.type() 加载 Java 类,并直接使用返回的值作为 Python 对象来调用 findFibonacci() 方法,通过传递一个参数。然后我们能够解析方法返回的结果。让我们编译 Java 代码并运行 Python 代码。以下显示了终端输出:
javac FibonacciCalculator.java
graalpython --jvm --vm.cp=. FibCal.py
Fibonacci number
0
1
2
3
5
8
13
21
34
我们可以看到,我们能够调用 Java 方法并获取一个整数数组,然后遍历它,而不需要任何额外的转换代码。
现在,让我们创建一个简单的 Python 函数,该函数使用 NumPy 对数据集进行快速分析。NumPy 是一个用于数组/矩阵操作的高性能 Python 库,在机器学习中得到广泛应用。为了欣赏 Graal 多语言的价值,想象一个用例,其中我们有一个包含各种心脏病病例信息的数据集,按年龄、性别、胆固醇水平、胸痛程度等组织,我们想了解在胸痛程度达到 3 级(高)后心脏病发作的人的平均年龄。这就是本节我们将要构建的内容,以了解 Java 和 Python 之间的多语言互操作性,以及我们如何使用 NumPy Python 库。
我们将使用 Kaggle 上提供的用于心脏病分析的数据集(www.kaggle.com/rashikrahmanpritom/heart-attack-analysis-prediction-dataset)。这个数据集包含了各种心脏病病例的信息,包括年龄、胆固醇水平、性别、胸痛程度等。以下是执行分析的 Python 代码:
import site
import numpy as np
import polyglot as poly
def heartAnalysis():
heartData = np.genfromtxt('heart.csv', delimiter=',')
dataOfPeopleWith3ChestPain = heartData[np.where(heartData[:,2]>2)]
averageAgeofPeopleWith3ChestPain = np.average(dataOfPeopleWith3ChestPain[:,0])
# Average age of people who are getting level 3 and greater chest pain
return averageAgeofPeopleWith3ChestPain
poly.export_value("hearAnalysis", heartAnalysis)
在前面的代码中,我们将 CSV 文件加载到一个矩阵中。在这里,我们特别关注第三列(索引为 2)。我们正在加载所有第三列值大于 2 的行,并将其存储在另一个变量中。然后我们计算这个矩阵的平均值并返回它。如果我们不得不在 Java 中做同样的事情,这将需要大量的代码。现在,让我们从 Java 中调用这段代码。
在下面的 Java 代码中,我们将通过 Binding 对象使用键导入函数定义。以下是完整的 Java 代码:
public class NumPyJavaExample {
public void callPythonMethods() {
Context ctx =
Context.newBuilder().allowAllAccess(true).build();
try {
File fibCal = new File("./numpy-example.py");
ctx.eval(Source.newBuilder("python", fibCal).build());
Value hearAnalysisFn = ctx.getBindings("python") .getMember("heartAnalysis");
Value heartAnalysisReport = hearAnalysisFn.execute();
System.out.println( "Average age of people who are getting level 3 and greater chest pain :" + heartAnalysisReport.toString());
} catch (Exception e) {
System.out.println("Exception : " );
e.printStackTrace();
}
}
public static void main(String[] args) {
NumPyJavaExample obj = new NumPyJavaExample();
obj.callPythonMethods();
}
}
在前面的 Java 代码中,我们创建了一个 Context 对象,并在 numpy-example.py 中评估 Python 代码。然后我们通过绑定访问函数定义,调用 Python 函数并能够获取值。我们打印返回的值。以下是运行此 Java 代码的输出:
$ java NumPyJavaExample
Average age of people who are getting level 3 and greater chest pain :55.869565217391305
在前面的输出中,我们可以看到第一次调用花费了时间,然而,随后的调用几乎不需要时间就能执行。这不仅展示了我们如何从 Java 代码中与 Python 代码进行互操作,还展示了 Truffle 和 Graal 如何优化执行。
在本节中,我们探讨了 Java 和 Python 的互操作性。在下一节中,我们将探讨 Python 与动态语言之间的互操作性。
探索 Python 与其他动态语言之间的互操作性
要探索 Python 与其他动态语言之间的互操作性,让我们使用上一节中使用的相同 numpy-example.py 文件。让我们从 JavaScript 中调用此方法。
以下是在 JavaScript 中调用 Python 代码的示例:
function callNumPyExmple() {
Polyglot.evalFile('python', './numpy-example.py');
heartAnalysis = Polyglot.import('heartAnalysis');
result = heartAnalysis();
return result;
}
result = callNumPyExmple();
print ('Average age of people who are getting level 3 and greater chest pain : '+ String(result));
在前面的代码中,我们可以看到我们是如何使用 Polyglot.import() 函数在 JavaScript 中导入 Python 的 heartAnalysis() 函数的。这返回了我们打印的平均值。让我们运行此代码,我们可以看到以下结果:
$ js --polyglot numpy-caller.js
Average age of people who are getting level 3 and greater chest pain : 55.869565217391305
现在我们来创建 JavaScript 代码,它将包含计算平方的函数。为了演示如何从 Python 调用 JavaScript 代码,以下是 JavaScript 代码:
var helloMathMessage = " Hello Math.js";
function square(a) {
return a*a;
}
Polyglot.export('square', square);
Polyglot.export('message', helloMathMessage)
这是一个非常简单的 JavaScript 函数,它返回传入值的平方。我们还导出了 square() 函数和一个变量 message,它携带 helloMathMessage 变量的值。
现在,让我们从 Python 代码中调用此方法。以下是将导入和调用前面 JavaScript 方法的 Python 代码:
import polyglot
polyglot.eval(path="./math.js", language="js")
message = polyglot.import_value('message')
square = polyglot.import_value('square')
print ("Square numbers by calling JS->Python: " + str(square(10, 20)))
print ("Hello message from JS: " + message)
在此代码中,我们使用 Python 的 polyglot 对象来评估 JavaScript 文件。然后我们通过调用 polyglot.import_value() 函数导入所有导出的函数/变量,使用 JavaScript 导出函数或变量时使用的相同键。然后我们能够调用这些函数并访问 message 变量并打印值。以下是在运行前面的代码后得到的输出:
$ graalpython --jvm --polyglot mathUser.py
Square numbers by calling JS->Python: 100
Hello messagr from JS: Hello Math.js
我们可以看到 Python 代码是如何导入和调用 JavaScript 代码的。这证明了双向互操作性。代码与其他语言,如 R 和 Ruby,非常相似。
在本节中,我们探讨了 Python 解释器如何与 Truffle 一起在 GraalVM 上运行以实现最佳性能。现在让我们探索并理解 GraalVM 上的 R 语言解释器。
理解 FastR – R Truffle 解释器
GraalVM 为兼容 GNU 的 R 运行时提供了一个 R Truffle 解释器。此运行时支持 R 程序和 REPL(读取-评估-打印循环)模式,在此模式下,我们可以在编写代码的同时快速测试代码。FastR 是开发此 R 运行时的项目。
安装和运行 R
就像 Graal Python 一样,R 运行时默认不包含在 GraalVM 中。我们必须使用 Graal Updater 下载和安装它。使用以下命令下载和安装 R 和 Rscript:
gu install r
要运行 R,我们需要 OpenMP 运行时库。在 Ubuntu 上可以使用apt-get install libcomp1安装,在 Oracle Linux 上使用yum install libcomp安装。在 macOS 上默认已安装该库。除此之外,如果 R 代码中有 C/C++/Fortran 代码,您还需要 C/C++/Fortran。在撰写本书时,R 还处于实验阶段,因此并非所有功能都得到支持。请参阅 GraalVM 文档(docs.oracle.com/en/graalvm/enterprise/20/docs/reference-manual/r/)以获取最新信息。
现在我们来测试 R。为了探索 R 解释器,让我们以交互式模式运行它。以下终端输出显示了测试 R 安装的交互式模式:
R
R version 3.6.1 (FastR)
Copyright (c) 2013-19, Oracle and/or its affiliates
Copyright (c) 1995-2018, The R Core Team
Copyright (c) 2018 The R Foundation for Statistical Computing
Copyright (c) 2012-4 Purdue University
Copyright (c) 1997-2002, Makoto Matsumoto and Takuji Nishimura
All rights reserved.
FastR is free software and comes with ABSOLUTELY NO WARRANTY.
You are welcome to redistribute it under certain conditions.
Type 'license()' or 'licence()' for distribution details.
R is a collaborative project with many contributors.
Type 'contributors()' for more information.
Type 'q()' to quit R.
[Previously saved workspace restored]
我们看到我们正在使用先前的输出中列出的 FastR GraalVM 版本。现在让我们通过运行一些 Python 命令来测试我们的 FastR 解释器是否正常工作,如下所示:
> 1+1
[1] 2
> abs(-200)
[1] 200
我们可以看到它正在交互式地提供结果。现在让我们绘制一个简单的示例。最好的方法是调用example(),这将显示图表,如下所示:
> example (plot)
plot> require(stats) # for lowess, rpois, rnorm
plot> plot(cars)
plot> lines(lowess(cars))
NULL
plot> plot(sin, -pi, 2*pi) # see ?plot.function
NULL
plot> ## Discrete Distribution Plot:
plot> plot(table(rpois(100, 5)), type = "h", col = "red", lwd = 10,
plot+ main = "rpois(100, lambda = 5)")
NULL
plot> ## Simple quantiles/ECDF, see ecdf() {library(stats)} for a better one:
plot> plot(x <- sort(rnorm(47)), type = "s", main = "plot(x, type = \"s\")")
plot> points(x, cex = .5, col = "dark red")
这将弹出一个包含绘制图表的窗口。以下图显示了弹出的图表截图:

图 8.4 – R 绘图输出截图
在撰写本书时,运行先前的plot命令时出现了一些警告。这些警告列出了 FastR 的一些限制。然而,这可能在未来的版本中发生变化。以下为出现的警告:
NULL
Warning messages:
1: In lines.default(lowess(cars)) :
lines.default not supported. Note: FastR does not support graphics package and most of its functions. Please use grid package or grid based packages like lattice instead.
2: In plot.function(sin, -pi, 2 * pi) :
plot.function not supported. Note: FastR does not support graphics package and most of its functions. Please use grid package or grid based packages like lattice instead.
3: In axis(...) :
axis not supported. Note: FastR does not support graphics package and most of its functions. Please use grid package or grid based packages like lattice instead.
4: In points.default(x, cex = 0.5, col = "dark red") :
points.default not supported. Note: FastR does not support graphics package and most of its functions. Please use grid package or grid based packages like lattice instead.
>
现在我们可以看到 R 运行正常,现在让我们探索 FastR 的互操作性功能。
探索 R 的互操作性
在本节中,为了探索多语言与 R 的互操作性,我们将运行一些内联 JavaScript,并加载示例 JavaScript 代码以及导入导出的函数和变量。我们将使用 R 交互式模式来完成此操作,以便更容易理解。要在多语言模式下运行 R,我们必须传递--polyglot参数。以下为命令:
R --polyglot
这将启动 R 的交互式运行时,并输出以下内容:
R version 3.6.1 (FastR)
Copyright (c) 2013-19, Oracle and/or its affiliates
Copyright (c) 1995-2018, The R Core Team
Copyright (c) 2018 The R Foundation for Statistical Computing
Copyright (c) 2012-4 Purdue University
Copyright (c) 1997-2002, Makoto Matsumoto and Takuji Nishimura
All rights reserved.
FastR is free software and comes with ABSOLUTELY NO WARRANTY.
You are welcome to redistribute it under certain conditions.
Type 'license()' or 'licence()' for distribution details.
R is a collaborative project with many contributors.
Type 'contributors()' for more information.
Type 'q()' to quit R.
[Previously saved workspace restored]
>
现在,让我们从简单的内联 JavaScript 开始:
> x <- eval.polyglot('js','[100,200,300,400]')
> print(x)
[polyglot value]
[1] 100 200 300 400
> print(x[3])
[1] 300
在先前的交互会话中,我们正在调用eval.polyglot()函数,其中包含语言 ID 和表达式。在这种情况下,我们将其指定为 JavaScript,语言 ID 为js,然后传递一个元素数组。然后我们打印数组及其第三个元素。eval.polyglot()函数提供多语言上下文并运行其他语言代码。现在让我们加载一个简单的 JavaScript 代码文件。以下为math.js的代码:
var helloMathMessage = " Hello Math.js";
function add(a, b) {
print("message from js: add() called");
return a+b;
}
function subtract(a, b) {
print("message from js: subtract() called");
return a-b;
}
function multiply(a, b) {
print("message from js: multiply() called");
return a*b;
}
Polyglot.export('add', add);
Polyglot.export('subtract', subtract);
Polyglot.export('multiply', multiply);
Polyglot.export('message', helloMathMessage)
前面的代码非常直接。我们定义了add()、subtract()和multiply()函数以及一个简单的变量message,它有一个字符串值Hello Math.js。然后我们使用Polyglot.export()将其导出,以便其他语言可以访问这些函数和变量。
现在让我们加载这个 JavaScript 文件并执行导出的代码;我们将在交互模式下运行指令。你将在这里找到交互会话,其中解释了我们正在做什么:
> mathjs <- eval.polyglot('js', path='/chapter8/r/math.js')
此指令加载 JavaScript 文件。确保路径已更新为包含 JavaScript 文件的精确路径。现在让我们将导出的函数和变量导入 R:
> message <- import('message')
> add <- import('add')
> subtract <- import('subtract')
> multiply <- import('multiply')
在前面的说明中,我们正在使用import()函数导入导出的函数和变量。使用与 JavaScript 文件中导出时相同的字符串非常重要。这些导入被分配给一个变量。现在让我们调用这些函数并打印变量:
> add(10,20)
message from js: add() called
[1] 30
> subtract(30,20)
message from js: subtract() called
[1] 10
> multiply(10,40)
message from js: multiply() called
[1] 400
> print(message)
[1] " Hello Math.js"
>
如你所见,我们可以调用 JavaScript 函数并打印变量。这展示了我们可以如何使用 JavaScript,但我们同样可以使用所有其他 Truffle 语言。现在让我们探索如何从 R 访问 Java 类。以下是HelloRPolyglot类的代码,我们将从 R 调用它:
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
public class HelloRPolyglot {
public String hello(String name) {
System.out.println("Hello Welcome from hello");
return "Hello Welcome from hello " + name;
}
public static void helloStatic() {
System.out.println("Hello from Static hello()");
try {
Context polyglot = Context.create();
Value array = polyglot.eval("js", "print('Hello from JS inline in HelloRPolyglot class')");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
HelloRPolyglot.helloStatic();
}
}
让我们理解前面的代码。我们有一个静态方法helloStatic(),它调用内联 JavaScript,打印一条消息,我们还有一个另一个方法hello(),它接受一个参数并打印一条hello消息。
让我们编译并运行 Java 类以测试它是否运行正常。以下显示了控制台输出:
javac HelloRPolyglot.java
java HelloRPolyglot
Hello Welcome to R Polyglot!!!
Hello from JS inline in HelloRPolyglot class
现在类运行正常,让我们开始 R 交互模式。这次,我们必须传递--jvm参数,让 R 运行时知道我们将使用 Java,并且还需要传递--vm参数,将CLASSPATH设置为包含 Java 类文件的当前目录:
R --jvm --vm.cp=.
R version 3.6.1 (FastR)
Copyright (c) 2013-19, Oracle and/or its affiliates
Copyright (c) 1995-2018, The R Core Team
Copyright (c) 2018 The R Foundation for Statistical Computing
Copyright (c) 2012-4 Purdue University
Copyright (c) 1997-2002, Makoto Matsumoto and Takuji Nishimura
All rights reserved.
FastR is free software and comes with ABSOLUTELY NO WARRANTY.
You are welcome to redistribute it under certain conditions.
Type 'license()' or 'licence()' for distribution details.
R is a collaborative project with many contributors.
Type 'contributors()' for more information.
Type 'q()' to quit R.
[Previously saved workspace restored]
>
现在 R 已加载,让我们运行调用 Java 类中hello()方法的指令。我们使用java.type()函数来加载类。以下是在线会话:
> class <- java.type('HelloRPolyglot')
> print(class)
[polyglot value]
$main
[polyglot value]
$helloStatic
[polyglot value]
$class
[polyglot value]
在前面的交互会话中,我们可以看到类已成功加载,当我们打印类时,我们看到它列出了其中的各种方法。现在让我们创建这个类的实例。我们使用new()函数来做这件事。以下是在使用new()函数的交互会话的输出:
> object <- new(class)
> print(object)
[polyglot value]
$main
[polyglot value]
$helloStatic
[polyglot value]
$class
[polyglot value]
$hello
[polyglot value]
在前面的代码中,我们可以看到对象已成功创建,因为它打印了类中的所有方法。现在让我们调用这些方法。我们将使用类来调用静态方法,并通过传递参数来调用hello()方法。以下是在线会话的输出:
> class$helloStatic()
Hello from Static heloo()
Hello from JS inline in HelloRPolyglot class
NULL
> object$hello('FastR')
Hello Welcome from hello
[1] "Hello Welcome from hello FastR"
>
在前面的会话中,我们可以看到调用两个方法的输出。
让我们用一个现实生活中的例子来说明我们如何使用 R 绘制图表的强大功能,并在 Node.js 生成的网页中使用绘制的图表。在本书的早期章节中,我们使用了一个从 Kaggle 获取的数据集,其中包含心脏病数据。让我们使用这个数据集在由 Node.js 生成的网页上绘制一个比较人们年龄和胆固醇水平的图表。
让我们使用 npm init 初始化一个 Node.js 项目。以下是在控制台输出的输出,其中我们提供了项目的名称和其他项目参数:
$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.
See `npm help init` for definitive documentation on these fields and exactly what they do.
Use `npm install <pkg>` afterwards to install a package and save it as a dependency in the package.json file.
Press ^C at any time to quit.
package name: (plotwithr-node)
version: (1.0.0)
description:
entry point: (plotWithR.js)
test command:
git repository:
keywords:
author:
license: (ISC)
About to write to /Users/vijaykumarab/AB-Home/Developer/GraalVM-book/Code/chapter8/r/plotWithR-node/package.json:
{
"name": "plotwithr-node",
"version": "1.0.0",
"description": "",
"main": "plotWithR.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
Is this OK? (yes)
这应该会生成一个 Node.js 模板。我们需要 Express.js 库来公开 REST 端点。现在让我们安装 express 库并使用 --save 来更新 package.json 文件,添加依赖项。以下是输出:
$ npm install express --save
added 50 packages, and audited 51 packages in 2s
found 0 vulnerabilities
现在我们来编写 Node.js 代码来加载数据集(heart.csv)并将条形图渲染为 scalar vector graph(SVG)。为了绘图,我们将使用 Lattice 包(你可以在 www.statmethods.net/advgraphs/trellis.html 找到更多关于这个库的详细信息)。
所以,以下是 Node.js 代码:
const express = require('express')
const app = express()
app.get('/plot', function (req, res) {
var text = ""
text += Polyglot.eval('R',
`svg();
require(lattice);
data <- read.csv("heart.csv", header = TRUE)
print(barchart(data$age~data$chol, main="Age vs Cholestral levels"))
grDevices:::svg.off()
`);
res.send(text)
})
app.listen(3000, function () {
console.log('Plot with R - listening on port 3000!')
})
让我们分析代码来理解它。我们正在加载 Express.js 并定义一个 '/plot' 端点。我们使用 Polyglot.eval() 来运行我们的 R 代码。我们初始化 SVG 并加载 Lattice 包。然后我们加载 heart.csv 文件并将图表渲染为条形图,然后将生成的 SVG 响应添加到 HTML 响应中,作为 /plot 端点的响应。
现在我们来运行这段代码。以下是运行代码后的输出:
node --jvm --polyglot plotWithR.js
Plot with R - listening on port 3000!
Loading required package: lattice
访问 http://locahost:3000/plot 来调用端点,在浏览器中。以下是一个输出截图:
![图 8.5 – 调用 /plot 的输出]
图 8.5 – 调用 /plot 的输出
R 是一种非常强大的用于统计计算和机器学习的语言。这为我们提供了在多种其他语言中嵌入 R 代码或调用 R 代码的机会。如果我们不得不在 Java 中执行相同的逻辑,可能需要付出很多努力。
摘要
在本章中,我们详细介绍了 Python、R 和 Java 在 Truffle 解释器中的实现方式。我们还探讨了这些语言提供的多语言互操作性功能,并附上了编码示例。我们了解了每种语言解释方式的不同。本章提供了一个实际操作指南,说明了如何在各种语言中运行代码和编写多语言应用程序。我们使用了非常简单的代码,以便你能够轻松理解概念和 API 以实现多语言应用程序。
你应该能够使用这些知识在 GraalVM 上编写多语言应用程序。尽管在撰写本书时,这些语言中的大多数仍然处于实验阶段,但它们提供了构建高性能多语言应用程序的绝佳机会。
在下一章中,你将获得丰富的实践经验和对多语言工作原理的理解,了解如何在 GraalVM 上构建 Python 和 R 应用程序,以及如何在这些程序之间进行交互。你还将对 GraalVM 的新运行时,Truffle 上的 Java 有一个良好的了解。
问题
-
什么是 Truffle 上的 Java?
-
Truffle 上的 Java 有什么优势?
-
Polyglot.cast()方法有什么用途? -
SST 和 ST 是什么?
-
.pyc文件是什么? -
GraalPython 中用于交换数据和函数定义的多语言绑定方法是什么?
-
你如何在 R 中导入其他语言定义?
-
你如何在 R 中加载 Java 类?
进一步阅读
-
GraalVM 企业版:
docs.oracle.com/en/graalvm/enterprise/19/index.html -
GraalVM 语言参考:
www.graalvm.org/reference-manual/languages/.
第十一章:GraalVM 多语言 – LLVM、Ruby 和 WASM
在上一章中,我们介绍了 Java、Python 和 R 的 Truffle 解释器以及语言间的互操作性。在本章中,我们将介绍其他语言的实现,例如以下内容:
-
LLVM:LLVM Truffle 解释器
-
TruffleRuby:Ruby 语言解释器实现
-
WebAssembly (WASM):WebAssembly 实现
所有这些语言实现目前都处于实验性阶段,并且在撰写本书时并未发布用于生产。然而,我们将探索其功能和构建一些代码来理解各种概念。
在本章中,我们将介绍以下主题:
-
理解 LLVM、Ruby 和 WASM 解释器及其多语言特性
-
理解这些各种语言解释器的兼容性和限制
到本章结束时,你将会有使用 LLVM、Ruby 和 WASM 解释器构建多语言应用程序的实践经验。
技术要求
本章需要以下内容以跟随各种编码/实践部分:
-
GraalVM 的最新版本。
-
各种语言的 Graal 运行时。我们将在本章中介绍如何安装和运行这些运行时。
-
访问 GitHub。在 Git 仓库中有一些可用的示例代码片段。代码可以从以下链接下载:
github.com/PacktPublishing/Supercharge-Your-Applications-with-GraalVM/tree/main/Chapter09。 -
本章的“代码在行动”视频可以在
bit.ly/3hT7Z1A找到。
理解 LLVM – (Sulong) Truffle 接口
LLVM 是一个编译器基础设施,它提供了一组模块化、可重用的编译器组件,可以形成一个工具链,将源代码编译成机器代码。该工具链在中间表示(IR)上提供各种级别的优化。任何源语言都可以使用这个工具链,只要源代码可以表示为 LLVM IR。一旦源代码表示为 LLVM IR,该语言就可以利用 LLVM 提供的高级优化技术。您可以参考 llvm.org/ 上的 LLVM 项目。已经基于这个基础设施构建了各种编译器。其中一些最受欢迎的是 Clang(用于 C、C++ 和 Objective C)、Swift(被苹果广泛使用)、Rust 和 Fortran。
Sulong 是一个用 Java 编写的 LLVM 解释器,内部使用 Truffle 语言实现框架。这使得所有可以生成 LLVM IR 的语言编译器都可以直接在 GraalVM 上运行。以下图表显示了 Sulong 如何使 LLVM 语言能够在 GraalVM 上运行:


图 9.1 – LLVM 编译管道
前面的图示展示了 LLVM 在非常高级的层面上是如何工作的。C/C++ 源代码被编译成 LLVM IR。让我们更好地理解这个图:
-
C/C++ 代码由 Clang 编译成 LLVM IR。
-
lliGraalVM LLVM IR 解释器有两个版本:原生和管理型。原生是默认的lli版本,它既包含在社区版中也包含在企业版中。管理型lli仅在企业版中可用,并提供了一种管理执行模式,我们将在 理解 LLVM 管理环境 部分中介绍。 -
LLVM IR 解释器执行初始优化并与 Truffle 和 Graal 集成,以便在运行时进行进一步优化。
为了理解 LLVM IR 的外观,这里有一些示例 C 代码,其中我们添加两个数字并返回结果:
int addInt(int a, int b)
{
return a + b;
}
当我们通过 Clang 处理它时,将生成以下 LLVM IR。我们可以使用 clang -S -emit-llvm cfile.c 命令来生成 LLVM IR:
define dso_local i32 @addInt(i32 %0, i32 %1) #0 !dbg !7 {
%3 = alloca i32, align 4
%4 = alloca i32, align 4
store i32 %0, i32* %3, align 4
call void @llvm.dbg.declare(metadata i32* %3,
metadata !12, metadata!DIExpression()), !dbg !13
store i32 %1, i32* %4, align 4
call void @llvm.dbg.declare(metadata i32* %4,
metadata !14, metadata !DIExpression()), !dbg !15
%5 = load i32, i32* %3, align 4, !dbg !16
%6 = load i32, i32* %4, align 4, !dbg !17
%7 = add nsw i32 %5, %6, !dbg !18
ret i32 %7, !dbg !19
}
前面的 IR 清楚地显示了代码是如何转换为 a 变量(%0),它作为参数传递,而 b (%1) 分别分配到 %3 和 %4。然后,值被加载到 %5 和 %6 中,并加到 %7。%7 中的值被返回。SSA 使得优化算法易于实现,因为算法不需要跟踪单个变量的值变化,因为变量的每个变化都分配给一个静态值。
LLVM IR 可以传递给 Sulong,它是 LLVM IR 解释器,它内部使用 Truffle 语言实现框架在 GraalVM 上运行代码。这利用了 GraalVM 的高级优化功能。
安装 LLVM 工具链
LLVM 可作为可选的运行时,可以使用 Graal Updater 安装,命令如下:
gu install llvm-toolchain
LLVM 被安装到 $GRAALVM_HOME/languages/llvm/native/bin。我们可以使用以下命令来检查路径:
$GRAALVM_HOME/bin/lli --print-toolchain-path
这将打印出 LLVM 安装的路径。企业版也附带了一个管理的 LLVM。我们将在本章后面介绍这一点。
GraalVM LLVM 运行时可以执行转换为 LLVM 位码的语言代码。GraalVM lli 工具解释位码,然后使用 Graal lli 动态编译。lli 还支持与动态语言的互操作性。lli 命令的语法将在下面展示:
lli [LLI options] [GraalVM options] [polyglot options] <bitcode file> [program args]
lli 可以执行纯位码或嵌入位码的原生可执行文件(Linux:ELF 和 macOS:Mach-O)。
让我们用一些简单的代码快速验证安装:
#include <stdio.h>
int main() {
printf("Welcome to LLVM Graal \n");
return 0;
}
让我们用 Clang 编译这段代码:
clang HelloGraal.c -o hellograal
这将运行 hellograal 应用程序。让我们使用 lli 来运行它。lli 是 LLVM 解释器:
lli hellograal
下面的示例显示了输出:
lli hellograal
Hello from GraalVM!
我们可以直接执行/hellograal并得到相同的输出。这被称为原生执行。有时原生执行比lli更快,但我们无法获得lli提供的多语言特性。让我们来看一个更复杂的情况;让我们将FibonacciCalculator.java转换为 C 语言。以下是 C 语言的源代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
long fib(int i) {
int fib1 = 0;
int fib2 = 1;
int currentFib, index;
long total = 0;
for (index = 2; index < i; ++index)
{
currentFib = fib1 + fib2;
fib1 = fib2;
fib2 = currentFib;
total += currentFib;
}
printf("%ld \n", total);
return total;
}
int main(int argc, char const *argv[])
{
for (int i = 1000000000; i < 1000000010; i++)
{
struct timeval tv_start;
struct timeval tv_end;
long time;
gettimeofday(andtv_start, NULL);
fib(i);
gettimeofday(andtv_end, NULL);
time = (tv_end.tv_sec*1000000 + tv_end.tv_usec) - (tv_start.tv_sec*1000000 + tv_start.tv_usec);
printf("i=%d time: %10ld\n", i, time);
}
return 0;
}
让我们运行以下命令来创建一个可执行文件:
/Library/Java/JavaVirtualMachines/graalvm-ee-java11-21.0.0.2/Contents/Home/languages/llvm/native/bin/clang FibonacciCalculator.c -o fibonacci
我们必须确保我们使用正确的 Clang 版本。我们必须使用与 GraalVM LLVM 工具链一起提供的 Clang;否则,我们无法使用lli。如果我们使用普通的 Clang 并尝试用lli执行生成的二进制文件,我们会得到以下错误:
oplevel executable /fibonacci does not contain bitcode
at <llvm> null(Unknown)
一旦创建了二进制文件,我们就用lli执行它以使用 GraalVM JIT 编译功能。以下是使用lli执行时的输出:
llvm git:(main) lli fibonacci
-24641037439717
i=1000000000 time: 5616852
-24639504571562
i=1000000001 time: 5592305
-24640314634125
i=1000000002 time: 5598246
-24639591828533
i=1000000003 time: 1116430
-24639679085504
i=1000000004 time: 1092585
-24639043536883
i=1000000005 time: 1140553
-24638495245233
i=1000000006 time: 1117817
-24637311404962
i=1000000007 time: 1121831
-24635579273041
i=1000000008 time: 1103494
-24636958268145
i=1000000009 time: 1109705
让我们在图表上绘制这些结果,看看性能在迭代中是如何提高的。以下是这个图表:
![图 9.2 – GraalVM LLVM 性能图表]

图 9.2 – GraalVM LLVM 性能图表
我们可以看到在迭代中有一个显著改进。我们可以看到,最初运行速度较慢,但在第三次迭代后,性能提高了近六倍。
探索 LLVM 互操作性
在本节中,我们将探索 LLVM 的互操作性功能,并了解我们如何在 Java、LLVM(C)和 JavaScript 之间进行交互。
Java 和 LLVM 互操作性
让我们首先尝试从 Java 中调用FibonacciCalculator.c可执行文件。以下是FibonacciCalculatorLLVMEmbed.java的源代码:
import java.io.File;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;
public class FibonacciCalculatorLLVMEmbed {
public static void main(String[] args) {
try {
Context polyglot = Context.newBuilder() .allowAllAccess(true).build();
File file = new File("fibpoly");
Source source = Source.newBuilder("llvm", file).build();
Value fibpoly = polyglot.eval(source);
fibpoly.execute();
} catch (Exception e) {
e.printStackTrace();
}
}
}
源代码与我们用 JavaScript 所做的工作非常相似。我们正在创建一个Context对象,使用Source对象加载编译的文件,并使用SourceBuilder从二进制文件构建Source对象。然后使用Context对象评估Source对象,并最终执行它。
让我们编译这个 Java 文件,并用 GraalVM Java 运行它。以下代码显示了文件的输出:
java FibonacciCalculatorLLVMEmbed
Inside C code: 10944
Returned value to Java 10944
我们可以看到,我们能够从 Java 中调用 C 代码。在这种情况下,我们只是执行了 C 应用程序。现在让我们尝试直接调用成员函数并传递一个整数,并将总和作为long类型返回。
以下是修改后的 C 代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
long fib(int i) {
int fib1 = 0;
int fib2 = 1;
int currentFib, index;
long total = 0;
for (index = 2; index < i; ++index) {
currentFib = fib1 + fib2;
fib1 = fib2;
fib2 = currentFib;
total += currentFib;
}
printf("Inside C code: %ld \n", total);
return total;
}
之前的代码实现了我们之前用 Java 编写的FibonacciCalculator逻辑。
在本节中,我们探讨了如何从 Java 中调用 C 方法。现在让我们了解如何从动态语言,如 JavaScript 中调用 C 代码。
探索 JavaScript 和 LLVM 互操作性
让我们检查与 JavaScript 的互操作性。JavaScript 提供了一个非常简单的代码片段。以下是 JavaScript 代码(FibonacciCaller.js):
var fibpoly = Polyglot.evalFile("llvm" , "fibpoly");
var fib = fibpoly.fib(20);
print("Returned value to JS: "+ fib);
让我们按照以下方式运行此 JavaScript:
js --polyglot FibonacciCaller.js
Inside C code: 10944
Returned value to JS: 10944
我们可以看到,我们现在能够将数据传递给 C 代码并执行 C 方法。
理解 LLVM 托管环境
GraalVM 企业版提供了 LLVM 的管理环境。我们可以在/languages/llvm/managed目录下找到一个 LLVM 管理工具链的版本,而默认的非管理工具链可以在/languages/llvm/native目录下找到。
当使用lli的管理版本时,可以通过--llvm.managed标志启用管理环境或管理执行模式。在本节中,让我们了解执行管理模式是什么,以及为什么它对 LLVM 特别必要。
为了理解我们通常遇到的问题,让我们取一些非常简单的代码,ManagedLLVM.c。在以下代码中,我们故意尝试将字符数组复制到一个未初始化的char指针:
#include <stdlib.h>
#include <stdio.h>
int main() {
char *string;
char valueStr[10] = "Hello Graaaaaaal";
strcpy(string, valueStr);
printf("%s", string);
free(string);
return 0;
}
让我们编译这段代码;Clang 实际上警告我们valueStr的初始化不正确。valueStr仅定义了 10 个字符,但我们分配了超过 10 个字符。让我们假设我们忽略这些警告并继续。应用程序仍然可以构建并执行。以下是编译ManagedLLVM.c文件的输出:
/Library/Java/JavaVirtualMachines/graalvm-ee-java11-21.0.0.2/Contents/Home/languages/llvm/native/bin/clang ManagedLLVM.c -o managedllvm
ManagedLLVM.c:5:23: warning: initializer-string for char array is too long
char valueStr[10] = "Hello Graaaaaaal";
^~~~~~~~~~~~~~~~~~
ManagedLLVM.c:6:3: warning: implicitly declaring library function 'strcpy' with type
'char *(char *, const char *)' [-Wimplicit-function- declaration]
strcpy(string, valueStr);
^
ManagedLLVM.c:6:3: note: include the header <string.h> or explicitly provide a declaration for 'strcpy'
2 warnings generated.
ManagedLLVM.c:5:23: warning: initializer-string for char array is too long
char valueStr[10] = "Hello Graaaaaaal";
^~~~~~~~~~~~~~~~~~
ManagedLLVM.c:6:3: warning: implicitly declaring library function 'strcpy' with type
'char *(char *, const char *)' [-Wimplicit-function- declaration]
strcpy(string, valueStr);
^
ManagedLLVM.c:6:3: note: include the header <string.h> or explicitly provide a declaration for 'strcpy'
2 warnings generated.
如果我们忽略警告并仍然运行应用程序的二进制文件,我们显然会得到一个页面错误。这会杀死宿主进程并完全停止应用程序。这类问题会导致核心转储并使应用程序崩溃,在 C/C++等语言中,我们经常遇到这类问题。以下是我们在本地模式(直接本地和本地lli)运行代码时的输出:
./managedllvm
[1] 30556 segmentation fault ./managedllvm
GraalVM LLVM 管理执行模式提供了一种优雅地处理这些问题的方法。让我们用相同的代码,这次使用 Clang 的管理版本进行编译,并用lli的管理版本运行它。让我们用lli的管理版本运行应用程序的二进制文件:
llvm git:(main) lli --llvm.managed managedllvm
Illegal null pointer access in 'store i64'.
at <llvm> main(ManagedLLVM.c:6:112)
它仍然失败了,但这次不是段错误或崩溃;它抛出了一个异常。异常可以被捕获并优雅地处理。
为了更好地理解如何处理这种情况,让我们创建一个 Java 类(ManagedLLVM.java),它从 Java 调用managedllvm可执行文件并优雅地处理异常:
import java.io.File;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;
public class ManagedLLVM {
public static void main(String[] args) {
try {
Context polyglot = Context.newBuilder() .allowAllAccess(true) .option("llvm.managed", "true") .build();
File file = new File("managedLLVM");
Source source = Source.newBuilder("llvm", file).build();
Value mllvm = polyglot.eval(source);
mllvm.execute();
} catch (Exception e) {
System.out.println("Exception occured....");
e.printStackTrace();
}
}
}
注意,我们现在正在使用llvm.manage选项将Context对象设置为true。这对我们以管理执行模式运行可执行文件至关重要。让我们编译并运行这个 Java 应用程序:
javac ManagedLLVM.java
java ManagedLLVM
Exception occured....
Illegal null pointer access in 'store i64'.
at <llvm> main(ManagedLLVM.c:6:112)
at org.graalvm.sdk/org.graalvm.polyglot.Value. execute(Value.java:455)
at ManagedLLVM.main(ManagedLLVM.java:13)
我们可以看到,Java 应用程序现在能够捕获异常,我们可以在这里编写异常处理代码。此外,它并没有停止应用程序。这是在管理执行模式下运行 LLVM 的一个最伟大的特性,并且即使在多语言环境中也得到了支持。
Ruby 是另一种在 GraalVM 中有高性能解释器实现的编程语言。让我们在下一节探索并理解 TruffleRuby,Ruby 的 Truffle 实现。
理解 TruffleRuby – Ruby 的 Truffle 解释器
TruffleRuby 是建立在 Truffle 之上的 GraalVM 上 Ruby 编程语言的高性能实现。在本节中,我们将通过代码示例探索一些语言特定的概念,以获得对 GraalVM 上 Ruby 实现的良好理解。
安装 TruffleRuby
TruffleRuby 默认情况下也不包含 GraalVM 安装。您必须使用 Graal 更新工具下载并安装它。要安装 TruffleRuby,请使用以下命令:
gu install ruby
在安装 Ruby 之后,我们必须运行一些后安装脚本,以便OpenSSL C 扩展能够工作。我们需要运行post_install_hook.sh,您可以在ruby/lib/truffle目录下找到它。让我们通过一个简单的 Ruby 应用程序来测试安装:
print "enter a "
a = gets.to_i
print "Enter b "
b = gets.to_i
c = a + b
puts "Result " + c.to_s
上述代码接受用户输入的a和b的整数值,将数字相加,并将结果作为字符串打印出来。这是一个非常简单的 Ruby 应用程序来测试 Ruby。让我们在 TruffleRuby 上运行这个程序。以下是在终端上的输出:
truffleruby helloruby.rb
enter a 10
Enter b 20
Result 30
现在我们知道 TruffleRuby 已安装并正在运行,让我们了解 TruffleRuby 解释器是如何工作的。
理解 TruffleRuby 解释器/编译器管道
TrufflyRuby,像任何其他客座语言一样,是 Truffle 解释器实现。以下图显示了 TruffleRuby 解释器/编译器管道:


图 9.3 – TruffleRuby 编译器/解释器管道
解释器/编译器管道与其他客座语言非常相似。前面的图表没有捕捉到所有细节。请参阅第六章中的探索 Truffle 解释器/编译器管道部分,Truffle – 概述,以获取关于 Truffle 解释器和 Graal JIT 如何执行代码的更详细解释。TruffleRuby 解释器在解析后构建 AST,并执行优化,然后将代码提交给 Graal JIT,就像任何其他客座语言一样。然而,其中一个关键区别是它处理 C 扩展的方式。C 扩展是 Ruby 编程的一个组成部分,并且传统上,C 扩展被插入到 Ruby 解释器中。TruffleRuby 使用 LLVM 解释器来处理这个问题。这自然提供了多语言互操作性,我们可以使用其他 LLVM 语言,如 C++、Rust 和 Swift,而不仅仅是 C。由于 TruffleRuby 建立在 Truffle 之上,它引入了多语言互操作性。让我们探索 TruffleRuby 的多语言互操作性功能。
探索 TruffleRuby 的多语言互操作性
TruffleRuby 带来了 Truffle 的所有多语言互操作性功能,并实现了类似的 API。让我们在本节中探索这些功能。让我们编写一些简单的 Ruby 代码,调用我们在前几节中构建的导出简单数学函数和变量的 JavaScript 文件:
var helloMathMessage = " Hello Math.js";
function add(a, b) {
print("message from js: add() called");
return a+b;
}
function subtract(a, b) {
print("message from js: subtract() called");
return a-b;
}
function multiply(a, b) {
print("message from js: multiply() called");
return a*b;
}
Polyglot.export('add', add);
Polyglot.export('subtract', subtract);
Polyglot.export('multiply', multiply);
Polyglot.export('message', helloMathMessage);
以下是一个 Ruby 代码示例,展示了多语言能力:
arrayPy = Polyglot.eval("python", "[10, 10.23456, 'Array String element']")
puts arrayPy.to_s
lengthOfArray = arrayPy.size
puts "Iterating through the Python Array object of size " + lengthOfArray.to_s
for i in 0..lengthOfArray - 1
puts "Element at " + i.to_s + " is " + arrayPy[i].to_s
end
在前面的代码中,我们调用 JavaScript 代码创建一个包含三个元素的数组:一个整数、一个浮点数和一个字符串。我们使用Polyglot.eval()在线评估此 JavaScript 代码。然后我们遍历数组并打印值。在以下代码中,我们加载math.js并导入message变量以及add()、subtract()和multiply()函数。然后我们调用这些函数并打印结果:
Polyglot.eval_file("./math.js")
message = Polyglot.import("message")
addFunction = Polyglot.import_method("add")
subtractFunction = Polyglot.import_method("subtract")
multiplyFunction = Polyglot.import_method("multiply")
puts "Message from JS " + message
puts "Result of add(10,20) " + add(10,20).to_s
puts "Result of subtract(10,20) " + subtract(40,20).to_s
puts "Result of multiply(10,20) " + multiply(10,20).to_s
现在让我们运行这段代码。以下是输出:
truffleruby --polyglot mathJsCaller.rb
#<Python [10, 10.23456, 'Array String element']>
Iterating throught the Python Array object of size 3
Element at 0 is 10
Element at 1 is 10.23456
Element at 2 is Array String element
Message from JS Hello Math.js
message from js: add() called
Result of add(10,20) 30
message from js: subtract() called
Result of subtract(10,20) 20
message from js: multiply() called
Result of multiply(10,20) 200
我们可以看到我们能够遍历 JavaScript 数组,并且还能调用math.js JavaScript 代码中的函数。
现在,让我们探索如何与 Java 交互。我们使用Java.type()方法加载 Java 类。让我们使用FibonacciCalculator的一个稍微修改过的版本。以下是 Java 源代码:
public class FibonacciCalculator{
public int[] findFibonacci(int count) {
int fib1 = 0;
int fib2 = 1;
int currentFib, index;
int [] fibNumbersArray = new int[count];
for(index=2; index < count+1; ++index ) {
currentFib = fib1 + fib2;
fib1 = fib2;
fib2 = currentFib;
fibNumbersArray[index - 1] = currentFib;
}
return fibNumbersArray;
}
public void iterateFibonacci() {
long startTime = System.currentTimeMillis();
long now = 0;
long last = startTime;
for (int i = 1000000000; i < 1000000010; i++) {
int[] fibs = findFibonacci(i);
long total = 0;
for (int j=0; j<fibs.length; j++) {
total += fibs[j];
}
now = System.currentTimeMillis();
System.out.printf("%d (%d ms)%n", i , now - last);
last = now;
}
long endTime = System.currentTimeMillis();
System.out.printf("total: (%d ms)%n", System.currentTimeMillis() - startTime);
}
public static void main(String args[]) {
FibonacciCalculator fibCal = new FibonacciCalculator();
fibCal.iterateFibonacci();
}
}
我们定义了两个方法:findFibonacci()和iterateFibonacci()。findFibonacci()方法接受一个整数并返回按请求计数生成的斐波那契数。iterateFibonacci()迭代并生成大量斐波那契数,并计时以检查代码的性能。
以下代码是用于加载FibonacciCalculator类并调用findFibonacci(int count)方法的 Ruby 脚本。我们向此方法传递一个整数值,并得到一个整数数组。然后我们遍历数组并打印值。我们还调用了iterateFibonacci()来比较它与直接使用 Java 运行的性能:
fibclass = Java.type('FibonacciCalculator')
fibObject = fibclass.new
fibonacciArray = fibObject.findFibonacci(10)
for i in 0..fibonacciArray.size - 1
puts "Element at " + i.to_s + " is " + fibonacciArray[i] .to_s
end
puts "Calling iterateFibonacci()"
fibObject.iterateFibonacci()
要运行此 Ruby 脚本,我们需要在命令行中传递--jvm,并使用--vm.cp指向 Java 类可用的路径。以下是运行 TruffleRuby 的输出:
truffleruby --jvm --vm.cp=. fibonacciJavaCaller.rb
Element at 0 is 0
Element at 1 is 1
Element at 2 is 2
Element at 3 is 3
Element at 4 is 5
Element at 5 is 8
Element at 6 is 13
Element at 7 is 21
Element at 8 is 34
Element at 9 is 55
Calling iterateFibonacci()
1000000000 (2946 ms)
1000000001 (1011 ms)
1000000002 (1293 ms)
1000000003 (1016 ms)
1000000004 (1083 ms)
1000000005 (1142 ms)
1000000006 (1072 ms)
1000000007 (994 ms)
1000000008 (982 ms)
1000000009 (999 ms)
total: (12538 ms)
我们可以看到我们能够在 Ruby 中调用 Java 类并遍历 Java 数组,甚至iterateFibonacci()的性能也相当不错。让我们尝试将此与直接使用 Java 运行此 Java 类进行比较。以下是输出:
java FibonacciCalculator
1000000000 (2790 ms)
1000000001 (592 ms)
1000000002 (1120 ms)
1000000003 (927 ms)
1000000004 (955 ms)
1000000005 (952 ms)
1000000006 (974 ms)
1000000007 (929 ms)
1000000008 (923 ms)
1000000009 (924 ms)
total: (11086 ms)
我们可以看到 Ruby 的性能与直接运行 Java 相当。TruffleRuby 是性能最好的 Ruby 运行时之一。TruffleRuby 处于实验阶段,在撰写本书时;有关最新信息,请参阅www.graalvm.org/reference-manual/ruby/。
使用 Ruby 最大的优点之一是 RubyGems。Ruby 拥有一个庞大的库,这是开发者社区在一段时间内构建的。所有宝石都托管在 https://rubygems.org/。借助 GraalVM Polyglot,这为在 Java 或任何其他由 GraalVM 支持的语言中使用这些宝石开辟了巨大的机会。为了说明这一点,让我们在一个 Java 程序中使用一个宝石。有一个名为 math_engine 的宝石(rubygems.org/gems/math_engine)。它有一个非常有趣的方法来评估复杂的数学表达式。假设我们正在构建一个可以用来评估复杂表达式的复杂代数计算器。让我们在一个 Ruby 程序中使用这个宝石,并从 Java 中调用它。
让我们先安装宝石。为了安装宝石,让我们使用 Bundler(https://bundler.io/)。Bundler 是一个包管理器(在 Node.js 中相当于 npm)。要安装 Bundler,请使用 gem install 命令。以下是安装 Bundler 的输出:
gem install bundler
Fetching bundler-2.2.17.gem
Successfully installed bundler-2.2.17
1 gem installed
现在让我们创建一个 Gemfile。Bundler 使用 Gemfile 中的配置来安装所有包/宝石。(这相当于 npm 中的 package.json。)以下是 Gemfile 的源代码:
source 'https://rubygems.org'
gem 'math_engine'
我们提供了 Gem 仓库的源代码,并指定了依赖于我们 Ruby 模块的宝石。现在让我们运行 Bundler 来安装这些宝石。(应该在我们有 Ruby 程序和 Gemfile 的文件夹中执行 bundle install 命令。)
bundle install 命令将安装所有宝石。现在让我们使用 math_engine 宝石,并在 Ruby 中定义一个名为 eval() 的方法,该方法接受表达式,评估它,并返回结果:
require 'rubygems'
require 'math_engine'
def eval(exp)
engine = MathEngine.new
ret = engine.evaluate(exp)
puts(ret)
return ret.truncate(4).to_f()
end
Polyglot.export_method('eval')
在前面的源代码中,我们使用 Polyglot.export_method() 导出方法,以便其他语言可以访问它。现在让我们从一个 Java 程序中调用这个 eval() 方法。
以下是对应的 Java 源代码:
public class MathEngineExample {
public void evaluateExpression(String exp) {
Context ctx = Context.newBuilder() .allowAllAccess(true).build();
try {
File fibCal = new File("./math_engine_expression.rb");
ctx.eval(Source.newBuilder("ruby", fibCal).build());
Value evaluateFunction = ctx.getBindings("ruby").getMember("eval");
Double evaluatedValue = evaluateFunction.execute(exp).asDouble();
System.out.printf("Evaluated Expression : " + evaluatedValue.toString());
} catch (Exception e) {
System.out.println("Exception : " );
e.printStackTrace();
}
}
public static void main(String[] args) {
MathEngineExample obj = new MathEngineExample();
obj.evaluateExpression("20 * (3/2) + (5 * 5) / (100.5 * 3)");
}
}
在前面的 Java 代码中,我们使用 Context 对象来加载 Ruby 运行时和我们的 Ruby 程序。然后我们将 eval() 方法绑定并使用传递的表达式执行它。然后捕获值并将其转换为字符串以打印。在 main() 方法中,我们传递一个复杂的数学表达式。现在让我们编译并运行这段 Java 代码。以下是输出:
java MathEngineExample
0.30082918739635157545605306799e2
Evaluated Expression : 30.0829
第一个输出来自 Ruby 的 put_s(),下一个输出来自 Java。这为使用大量宝石库开辟了巨大的机会。
理解 TruffleRuby 与 CRuby 与 JRuby 的区别
Ruby 在市场上有很多实现。JRuby 和 CRuby 是 Ruby 的两种流行实现。
JRuby 是用 Java 实现的 Ruby 编程语言。CRuby 是用 C 实现的 Ruby 编程语言。以下图表显示了 JRuby 和 CRuby 的高级编译管道。JRuby 是 Ruby 性能最高的实现之一,因为它引入了优化和即时编译。JRuby 也没有像 CRuby 那样的全局解释器锁,这允许并发执行,因此速度更快。然而,JRuby 启动较慢,但随时间推移性能更佳:

图 9.4 – JRuby 和 CRuby 编译管道
TruffleRuby 的性能优于 JRuby 和 CRuby。请参阅www.graalvm.org/ruby/以获取更详细的 optcarrot 和 Rubycon 基准测试结果。
理解 GraalWasm – WASM Truffle 解释器
GraalVM 为 WASM 代码提供了一个解释器和编译器,称为 GraalWasm。GraalWasm 开辟了构建接近本地性能的多语言 Web 应用程序的可能性。在我们深入了解 GraalWasm 之前,让我们快速概述一下 WASM。
理解 WASM
WASM是一种可以在大多数现代浏览器上以接近本地速度运行的二进制格式。Web 应用程序变得越来越复杂,需要高性能和接近本地的体验。JavaScript 只能达到一定水平,我们已经看到了许多基于 JavaScript 的非常好的应用程序,它们提供了几乎本地的体验。WASM 增强了 JavaScript 和其他技术,使我们能够在 Web 上编译 C、C++和 Rust 程序。以下图显示了构建 WASM 应用程序的非常简单的管道:

图 9.5 – WASM 编译管道流程
在前面的图中,我们可以看到如何使用 Emscripten 将 C/C++代码编译成 WASM,并且它与其他 Web 技术共存,在浏览器上运行。JavaScript 和 WASM 都在浏览器上执行逻辑。主要区别在于 WASM 是以编译时已经优化过的二进制形式交付的。不需要抽象语法树和类型特化和推测(参考第六章中的探索 Truffle 解释器/编译器管道部分,Truffle – 概述,以了解更多关于 AST 和推测优化)。
这也是 WASM 具有更小体积和更快性能的原因之一。现代 Web 应用程序架构利用 WASM 在浏览器中执行更高级的计算逻辑,而 JavaScript 用于运行用户界面和简单的应用程序逻辑。通过引入多语言互操作性和嵌入,GraalWasm 开辟了更多可能性。让我们在下一节中探讨这一点。
理解 GraalWasm 架构
让我们了解 GraalWasm 的工作原理。以下图显示了 GraalWasm 的编译管道。该图并未捕捉到所有细节。请参考 第六章 中的 探索 Truffle 解释器/编译器管道 部分,Truffle – 概述,以获取关于 Truffle 解释器和 Graal JIT 如何执行代码的更详细解释:

图 9.6 – GraalWasm 编译/解释器管道
C/C++ 源代码使用 Emscripten (emcc) 进行编译。Emscripten 是基于 LLVM 的 gcc 或 Clang 的替代品。Emscripten 将源代码编译成 WASM (.wasm) 文件。GraalWasm 解释器创建 AST 并执行优化。由于 WASM 是从强类型语言生成的,因此不需要太多优化。由于 WASM 格式是一系列指令,为了减少内存占用,GraalWasm 构建一个 AST,其中每个节点都指向 WASM 中的一个块(这被称为 WASM 块节点),而不是为每条指令创建一个节点。然后 GraalWasm 解释器优化 AST 并将其传递给 Graal 以执行。
安装和运行 GraalWasm
GraalWasm 是一个可选组件;它不包含在 GraalVM 安装中。我们必须使用 Graal Updater 来安装它。要安装 GraalWasm,我们可以使用以下命令来下载和安装它:
gu install wasm
现在,让我们构建一个 WASM 二进制文件,并使用 GraalWasm 运行它。要将 C 代码编译成 WASM,我们必须安装 emcc。以下部分将介绍安装 emcc 的步骤。
安装 Emscripten (emcc)
Emscripten 使用 Emscripten SDK 进行安装。我们可以从 Git 仓库下载 SDK。让我们在终端执行以下命令:
git clone https://github.com/emscripten-core/emsdk.git
这将下载 emsdk,它还包含所有所需的安装脚本。git clone 将创建一个 emsdk 目录。让我们将其移动到 cd emsdk 文件夹并执行 git pull 以确保它是最新的。我们可以通过执行以下命令来安装 emcc 工具链:
./emsdk install latest
这将下载所有所需的工具链和 SDK。一旦下载完成,我们需要激活 emsdk 并然后设置环境,按照以下命令顺序执行:
./emsdk activate latest
./emsdk_env.sh
一旦所有这些命令都成功执行,我们应该能够通过在终端运行 emcc 命令来检查 Emscripten 是否已安装。
现在,让我们构建以下 C 代码的 WASM。这是对我们在 探索 LLVM 互操作性 部分中已经使用的代码的轻微修改:
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
static long fib(int i) {
int fib1 = 0;
int fib2 = 1;
int currentFib, index;
long total = 0;
for (index = 2; index < i; ++index) {
currentFib = fib1 + fib2;
fib1 = fib2;
fib2 = currentFib;
total += currentFib;
}
return total;
}
我们定义了一个计算斐波那契数的方法,将它们相加,并返回斐波那契数的总和。在以下 main() 方法的代码片段中,我们正在循环调用这个 fib() 方法进行交互,只是为了测试这个方法,并打印每次迭代的总时间和执行时间:
int main(int argc, char const *argv[])
{
for (int i = 10000; i < 10010; i++) {
struct timeval tv_start;
struct timeval tv_end;
long time;
gettimeofday(&tv_start, NULL);
fib(i);
gettimeofday(&tv_end, NULL);
time = (tv_end.tv_sec*1000000 + tv_end.tv_usec) – (tv_start.tv_sec*1000000 + tv_start.tv_usec);
printf("i=%d time: %10ld\n", i, time);
}
return 0;
}
让我们使用以下命令使用 emcc 编译此代码:
emcc -o fibonacci.wasm fibonacci.c
在上一个命令中,-o 选项用于指定输出文件的名称;在这种情况下,fibonacci.wasm 是在成功编译后生成的二进制文件。让我们通过执行以下命令来运行这个文件:
wasm --Builtins=wasi_snapshot_preview1 fibonacci.wasm
在前面的命令中,我们使用了 --Builtins 来传递 Emscripten 工具链所需的 wasi_snapshot_preview1 模块。以下是在执行后的输出:
wasm --Builtins=wasi_snapshot_preview1 fibonacci.wasm
i=10000 time: 6563
i=10001 time: 6519
i=10002 time: 6841
i=10003 time: 8455
i=10004 time: 6838
i=10005 time: 7156
i=10006 time: 7214
i=10007 time: 237
i=10008 time: 265
i=10009 time: 247
我们可以立即看到生成的输出,对于我们要创建和相加的斐波那契数的数量来说,速度相当快。此外,我们还可以看到从 6,519 毫秒到 247 毫秒的巨大性能提升。
摘要
在本章中,我们详细介绍了如何在 Truffle 上实现 LLVM、Ruby、WASM、Java 以及 Ruby 解释器。我们还探讨了这些语言提供的多语言互操作性功能,以及编码示例。我们了解了每种语言解释方式的不同。本章提供了在实际这些各种语言中运行代码和编写多语言应用程序的实战指南。
您应该能够使用这些知识在 GraalVM 上编写多语言应用程序。尽管在撰写本书时,这些语言中的大多数仍然处于实验阶段,但它们提供了构建高性能多语言应用程序的绝佳机会。在下一章中,我们将看到新的框架,如 Quarkus 和 Micronaut,如何实现 Graal 以实现最优化微服务架构。
问题
-
Sulong 是什么?
-
LLVM 执行的托管模式是什么?
-
TruffleRuby 如何实现 C 扩展?
-
WASM 是什么?
-
emcc是什么?
进一步阅读
-
GraalVM 企业版:
docs.oracle.com/en/graalvm/enterprise/19/index.html -
GraalVM 语言参考:
www.graalvm.org/reference-manual/languages/ -
GraalWasm 宣布博客文章:
medium.com/graalvm/announcing-graalwasm-a-webassembly-engine-in-graalvm-25cd0400a7f2 -
GraalVM 中的 WASM 引擎:介绍 Oracle 的 GraalWasm:
jaxenter.com/graalvm-webassembly-graalwasm-165111.html
第十二章:第四部分:使用 Graal 的微服务
本节将向您介绍如何使用 Graal 构建云原生应用作为微服务,以及这如何有助于无服务器架构。它还将涵盖一个案例研究,说明如何使用 Graal 与 Quarkus 和 Micronaut 一起构建微服务应用:
- 第十章, 使用 GraalVM 的微服务架构
第十三章:使用 GraalVM 的微服务架构
在前面的章节中,我们探讨了 GraalVM 是如何建立在 Java 虚拟机之上,并提供高性能的多语言运行时。在本章中,我们将探讨 GraalVM 如何成为运行微服务的核心运行时。许多微服务框架已经在 GraalVM 上运行。我们将探讨一些流行的框架,并使用它们构建一个示例应用程序。我们还将探讨一个无服务器框架。我们将进行案例研究,看看我们如何设计解决方案。
在本章结束时,您将深入了解如何将应用程序打包为容器,运行 GraalVM,以及如何使用 Micronaut、Quarkus 和 Spring Boot 构建微服务应用程序。本章假设您对 Java 编程语言有良好的理解,并对构建 Java 微服务有一些了解。
本章将涵盖以下主题:
-
GraalVM 微服务架构概述
-
了解 GraalVM 如何帮助构建微服务架构
-
构建微服务应用程序
-
一个案例研究,帮助理解如何解决基于 GraalVM 构建的微服务应用程序
-
使用 Spring Boot、Micronaut、Quarkus 和 Fn Project 无服务器框架实现微服务
技术要求
本章提供了构建 Java 微服务的实战指南。这需要安装和设置一些软件。以下是一个先决条件列表:
-
源代码: 本章中提到的所有源代码都可以从 Git 仓库
github.com/PacktPublishing/Supercharge-Your-Applications-with-GraalVM/tree/main/Chapter10下载。 -
GraalVM: 需要安装 GraalVM。有关安装和设置 GraalVM 的详细说明,请参阅
www.graalvm.org/docs/getting-started/#install-graalvm。 -
Spring Boot: 请参阅
spring.io/guides/gs/spring-boot/获取有关如何设置和使用 Spring Boot 的更多详细信息。 -
Micronaut: 我们将使用 Micronaut 框架来构建代码。请参阅
micronaut.io/download/获取有关如何下载和设置 Micronaut 的更多详细信息。 -
Quarkus: 我们将使用 Quarkus 框架来构建微服务。请参阅
quarkus.io/获取有关如何设置和使用 Quarkus 的更多详细信息。 -
fn project: 我们将使用 fn project 构建无服务器应用程序/函数。请参阅
fnproject.io/获取有关如何下载、安装和设置 fn project 的更多详细信息。 -
本章的“代码实战”视频可以在
bit.ly/3f7iT1T找到。
那么,让我们开始吧!
微服务架构概述
微服务是最受欢迎的架构模式之一,并且已被证明是云原生应用程序开发的最佳架构模式。微服务模式有助于将应用程序分解和结构化为更小、更易于管理和自包含的组件,这些组件通过标准服务接口公开功能。以下是一些微服务架构模式的优点:
-
松耦合:由于应用程序被分解为提供标准接口的服务,因此应用程序组件可以独立管理、升级和修复,而不会影响其他依赖组件。这有助于根据不断增长的业务需求和变化轻松更改应用程序逻辑。
-
可管理性:由于组件是自包含的,因此管理这些应用程序非常容易。组件可以由较小的团队负责开发,并且可以独立部署,而无需部署整个应用程序。这有助于使用 DevOps 进行快速开发和部署。
-
可扩展性:可扩展性是云原生应用程序的关键要求之一。在单体应用中,可扩展性是一个问题,因为即使我们只需要扩展功能的一部分,我们也必须扩展整个应用程序。例如,在需求高峰期间,我们可能希望比零售门户的其他任何功能更多地扩展订单、购物车和目录服务。在单体应用中这是不可能的,但如果将这些组件分解为独立的微服务,则可以轻松地单独扩展它们并设置自动扩展参数,以便根据需求进行扩展。这有助于更有效地利用云资源,同时降低成本。
让我们现在探讨 GraalVM 如何帮助构建高性能的微服务架构。
使用 GraalVM 构建微服务架构
GraalVM 非常适合微服务架构,因为它有助于构建具有较小内存占用和更快启动速度的高性能 Java 应用程序。微服务架构最重要的要求之一是较小的内存占用和更快的启动速度。GraalVM 是云中运行多语言工作负载的理想运行时。市场上已经有一些云原生框架,可以构建在 GraalVM 上运行优化的应用程序,包括 Quarkus、Micronaut、Helidon 和 Spring。
理解 GraalVM 容器
传统上,应用程序部署在预先配置和设置好的基础设施上,以便应用程序运行。该基础设施包括硬件和运行应用程序的软件平台。例如,如果我们需要运行一个 Web 应用程序,我们首先需要设置操作系统(例如 Linux 或 Windows)。Web 应用程序服务器(如 Tomcat、WebSphere)和数据库(如 MySQL、Oracle 或 DB2)设置在预定义的硬件基础设施上,然后应用程序部署在这些 Web 应用程序服务器之上。这需要花费很多时间,而且每次我们需要设置应用程序时,可能都要重复这种方法。
为了减少设置时间和使配置更容易管理,我们转向通过预包装应用程序、各种平台组件(如应用程序服务器、数据库等)和操作系统,将基础设施虚拟化,形成自包含的虚拟机(VMs)。(这些虚拟机不要与Java 虚拟机(JVM)混淆。JVM 更像是运行 Java 应用程序的平台。在此上下文中,虚拟机远不止是一个应用程序平台。)
虚拟化帮助解决了许多配置和部署问题。它还允许我们通过在同一台机器上运行多个虚拟机并更好地利用资源来优化硬件资源的利用率。虚拟机体积庞大,因为它们自带操作系统,难以快速部署、更新和管理。
容器化通过引入另一层虚拟化解决了这个问题。大多数现代架构都是基于容器的。容器是软件单元,它打包代码以及所有依赖和环境配置。容器是轻量级的独立可执行包,可以在容器运行时上部署。以下图表显示了虚拟机和容器之间的区别:

图 10.1 – 虚拟机与容器
GraalVM 是一个完美的应用程序平台(尤其是当它编译为原生代码时),可以与应用程序一起打包到同一个容器中。GraalVM 提供了最小的占用空间和更快的启动和执行速度,以便快速部署和扩展应用程序组件。
前面的图表显示了如何使用 GraalVM 对应用程序进行容器化。在先前的模型中,每个容器都有自己的虚拟机,该虚拟机具有内存管理、分析、优化(JIT)等逻辑。GraalVM 提供的是与容器运行时一起的通用运行时,仅将应用程序逻辑容器化。由于 GraalVM 还支持多种语言以及这些语言之间的互操作性,因此容器可以运行用多种语言编写的应用程序代码。
以下图表显示了容器与 GraalVM 一起部署的各种场景:
![Figure 10.2 – GraalVM container patterns]
![img/Figure_10.2_B16878.jpg]
图 10.2 – GraalVM 容器模式
在前面的图中,我们可以看到各种配置/场景。让我们详细了解:
-
容器 1:在这个容器中,我们可以看到一个原生镜像正在运行。这是迄今为止最优化配置,具有最小的占用空间和更快的加载速度。
-
容器 2:在这个容器中,我们有一个 Java 应用和一个在 Truffle 上运行的 JavaScript 应用,它们具有互操作性。
-
容器 3:与容器 2 类似,我们也可以看到一个 C/C++应用。
容器 1 是运行云原生应用的最优配置,除非我们编写了需要互操作的不同编程语言的应用代码。另一种方法是编译原生镜像并将它们拆分为单独的容器,然后使用标准协议如 REST 进行交互。
这些容器可以使用各种编排器在云中部署,例如 Docker Swarm、Kubernetes(包括 Azure Kubernetes Service、AWS Elastic Kubernetes Service 和 Google Kubernetes Engine)、AWS Fargate 以及 Red Hat OpenShift。
让我们通过案例研究来探讨如何使用 GraalVM 作为微服务架构中的通用运行时。
案例研究 – 在线图书库
要了解如何使用各种现代微服务框架在 GraalVM 上实现微服务,让我们通过一个非常简单的案例研究来探讨。在本章的后面部分,我们将从这个架构中选择一个服务并使用不同的框架来构建它。
本案例研究涉及构建一个简单的网站,展示图书目录。目录列出了所有图书。您可以通过特定的关键词搜索和浏览图书,并且应该能够选择并获取与图书相关的更多详细信息。用户可以选择并保存它作为图书馆中的愿望清单。在未来,这可以扩展为订购此书。但为了保持简单,让我们假设我们是在MVP(最小可行产品)范围内进行搜索、浏览和创建个人图书馆。我们还在目录中添加了一个部分,用户可以查看基于其图书馆内容的图书预测。这也有助于我们使用一些机器学习代码进行多语言处理。
功能架构
让我们通过构建此应用程序的思维过程。我们首先将开始分解功能。为此,我们需要以下服务:
-
目录 UI 服务:这是用户成功登录后到达的首页(在 MVP 中,我们不会实现登录、认证和授权)。这个网页提供了一个搜索和查看图书的方式。这将作为一个微前端实现(有关微前端的更多详细信息,请参阅
micro-frontends.org/)。我们将有三个 UI 组件如下:i. 书籍列表 UI 组件:此组件显示所有书籍的列表。
ii. 书籍详情 UI 组件:此组件显示与所选书籍相关的所有详细信息。
iii. 预测书籍 UI 组件:此组件显示基于图书馆中的书籍预测的书籍。
-
图书馆 UI 服务:此服务列出您个人图书馆中的书籍,并允许用户添加或删除此图书馆中的书籍。
现在,为了支持这些 UI 服务,我们需要存储、检索和搜索书籍的微服务。以下是我们需要的以下服务:
-
目录服务:这些服务提供 RESTful API 以浏览、搜索和查看书籍详情。
-
预测服务:为了展示 GraalVM 的多语言特性,让我们假设我们已经有使用 Python 开发的机器学习代码,并且可以根据图书馆中可用的书籍预测书籍。我们将在此 Java 微服务中嵌入此 Python 代码,以展示 GraalVM 如何帮助我们构建优化的多语言嵌入应用程序。
-
图书馆服务:此服务将提供所有用于访问图书馆书籍以及添加和删除书籍的 RESTful API。
-
书籍信息服务:让我们决定使用 Google Books API (
developers.google.com/books)来获取所有关于书籍的详细信息。我们需要一个代理 Google Books API 的服务。这将帮助我们管理来自 Google Books API 的数据。这还提供了一个代理层,这样我们就可以随时切换到不同的书籍 API 服务,而无需更改整个应用程序。
现在,我们需要存储来存储已添加到个人图书馆的书籍信息以及缓存书籍数据,以便更快地获取(而不是每次都调用 Google Books API)。为此,我们需要以下数据服务:
-
用户资料数据:此部分存储用户资料。
-
用户图书馆数据:此部分存储特定用户为其图书馆选择的书籍。
-
书籍缓存数据:我们需要缓存书籍信息,这样我们就不必为已经获取的信息调用 Google Books API。这不仅会提高性能;还会降低成本,因为 Google Books API 可能会根据调用次数向您收费。
以下图表说明了这些组件是如何协同工作的:
![图 10.3 – 书籍图书馆应用程序 – 功能架构
![img/Figure_10.3_B16878.jpg]
图 10.3 – 书籍图书馆应用程序 – 功能架构
在构建最终架构时,我们做出了各种架构决策。让我们快速回顾一下:
-
微前端: 我们决定将 UI 组件做成微前端,这样我们更容易管理和复用 UI 代码。正如我们所见,目录 UI 和图书馆 UI 都复用了相同的组件来渲染书籍列表并显示书籍详情。我们选择 ReactJS 作为微前端实现,因为它提供了一个非常稳定的框架。
-
嵌入 Python: 我们决定复用已经为预测构建的 Python 代码。我们决定将其嵌入到我们的目录服务中,以提供一个提供预测书籍列表的端点。这也有助于我们展示多语言的能力。我们将使用纯 Java 实现的微服务,因为大多数现代微服务框架都不支持多语言。
-
无服务器: 我们决定将书籍信息服务以无服务器的形式渲染,因为它不需要保持状态;它只需调用 Google Books API 并传递信息。
-
书籍信息缓存: 我们决定使用 Redis 来存储书籍信息缓存,这样我们就不必每次都回到 Google Books API,从而提高性能并减少调用 Google API 的成本。
现在我们来看看在 Kubernetes 上的部署架构将是什么样子。有关 Kubernetes 如何编排容器以及提供可扩展和高度可用解决方案的更多详细信息,请参阅kubernetes.io/。以下部分假设您对 Kubernetes 有很好的理解。
部署架构
容器部署在 Kubernetes 集群上。以下图表显示了这些容器在 Kubernetes 中的部署架构。这可以与任何云相似:

图 10.4 – 书库应用在 Kubernetes 上的部署架构
让我们更详细地了解前面图表中使用的术语:
-
8080,目标端口指向目录 UI 页面的集群 IP,即主页。 -
reactjs实现的主页和图书馆页,它们内部使用相同的reactjs组件集。这调用图书馆服务以获取有关个人图书馆中存储的书籍的信息。此服务还调用目录服务,该服务具有所有 REST 端点以搜索和浏览书籍详情。 -
图书馆服务部署: 图书馆服务是用 Quarkus 作为原生图像实现的,并提供访问个人图书馆信息的端点。这使用图书馆数据服务。
-
图书馆数据服务部署: 图书馆数据服务是一个 PostgreSQL 容器,存储所有用户资料和个人图书馆信息。它还使用持久卷,以便在节点故障时存储信息。
-
在 Quarkus 原生模式下,
CatalogueInfoService。该服务提供了搜索、浏览和获取与书籍相关的各种详细信息的端点。BookInfoService用于获取有关书籍的所有信息。CatalogueInfoService还使用BookInfoCache服务来获取已缓存的现有数据。 -
BookInfoService 部署:这个部署有一个无服务器实现服务,从 Google Books API 获取各种书籍信息。这将使用在 GraalVM 上运行的 fn 项目无服务器框架来实现。
-
BookInfoCacheService 部署:这个部署是一个 Redis 缓存,用于缓存所有书籍信息,以避免对 Google Books API 的重复调用。
最终的源代码可以在 Git 仓库中找到。我们不会讨论源代码,但为了更好地理解如何构建这些微服务,我们将在下一节中选取 BookInfoService 并使用各种微服务框架实现它。
探索现代微服务框架
有一些现代框架是围绕快速创建微服务而构建的。这些框架基于 容器优先 和 云优先 的设计原则。它们从头开始构建,具有快速的启动时间和低内存占用。Helidon、Micronaut 和 Quarkus 是最广泛使用的三个现代 Java 框架。所有这三个框架都在 GraalVM 上原生运行。每个框架都承诺更快的启动时间和低内存占用,并且它们通过不同的方法实现这一点。在本节中,我们将探讨这些框架。
为了理解这些框架,现在让我们动手构建一个简单的书籍信息服务。这是一个简单的服务,它接受一个关键字,使用 Google Books API 获取书籍信息,并返回与关键字匹配的所有书籍的详细信息。响应以 JSON(JavaScript 对象表示法)返回(有关更多详细信息,请参阅 www.json.org/json-en.html)。
让我们先从使用 Spring Boot 构建,但不使用 GraalVM 的传统微服务开始。
使用 Spring 构建 BookInfoService 而不使用 GraalVM
Spring 是最广泛使用的 Java 微服务框架之一。它提供了许多出色的功能,并且是构建云原生应用程序时使用的流行框架之一。在本节中,我们将以传统方式构建,不使用 GraalVM,以便理解传统方法的不足之处。
创建 Spring 模板代码
要创建 Spring 模板代码,请访问浏览器中的 start.spring.io/。该网站帮助我们指定一些配置并生成模板代码。让我们为我们的 BookInfoService 生成模板代码。以下截图显示了 Spring 初始化器:
![图 10.5 – 生成模板代码的 Spring Initializr 截图]

图 10.5 – 生成模板代码的 Spring Initializr 截图
上述截图显示了用于生成模板代码的配置。为了保持简单和专注,我们选择使用HttpClient调用 Google API,以便简化操作,而不是推荐使用 jsonb 等方法。
我们需要提取生成的 ZIP 文件,然后实现服务。以下是一个核心逻辑代码片段。完整代码可在 Git 仓库github.com/PacktPublishing/Optimizing-Application-Performance-with-GraalVM中找到:
@RestController
public class BookInfoServiceController {
@RequestMapping("/book-info")
public String bookInfo(@RequestParam String query) {
在前面的代码中,我们将路径设置为/book-info以调用BookInfoService。在以下代码中,我们将调用 Google API 以获取图书信息:
String responseJson ="{}";
try {
String url = "https://www.googleapis.com/ books/v1/volumes?q="+query+"&key=<your google api key>";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)).build();
HttpResponse<String> response;
response = client.send(request, BodyHandlers.ofString());
responseJson = response.body();
} catch (Exception e) {
responseJson = "{'error','"+e.getMessage()+"'}";
e.printStackTrace();
}
return responseJson;
}
}
在前面的代码中,我们使用我们的 Google API 密钥调用 Google 图书 API。您必须获取自己的密钥并将其包含在 URL 中。有关如何获取自己的 Google API 的更多详细信息,请参阅cloud.google.com/apis/docs/overview。我们使用HttpClient调用 Google 图书 API 并将响应传递给请求者。
现在让我们构建这段代码并运行它。我们将使用 Maven 来构建。以下命令将构建代码:
./mvnw package
这将下载所有依赖项,构建应用程序,并生成一个 JAR 文件。您将在 target 文件夹下找到 JAR 文件。我们可以使用以下命令运行 JAR 文件:
java -jar target/book-info-service-0.0.1-SNAPSHOT.jar
这将启动 Spring Boot 应用程序。以下截图显示了应用程序运行的输出:
![图 10.6 – Spring BookInfoService 应用程序的输出截图]

图 10.6 – Spring BookInfoService 应用程序的输出截图
现在,让我们使用 REST 客户端访问这个应用程序。在这种情况下,我们使用CocoaRestClient(mmattozzi.github.io/cocoa-rest-client/)。您可以使用任何 REST 客户端,甚至可以使用浏览器来调用服务。让我们调用 http://localhost:8080/book-info?query=graalvm。以下截图显示了输出:
![图 10.7 – 调用 BookInformationService Spring 应用程序的输出]

图 10.7 – 调用 BookInformationService Spring 应用程序的输出
既然我们知道应用程序正在运行,让我们将这个应用程序打包成一个 Docker 容器并构建镜像。以下是为构建镜像的 Dockerfile 代码:
FROM adoptopenjdk/openjdk11:ubi
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} bookinfo.jar
ENTRYPOINT ["java","-jar","/bookinfo.jar"]
这是一个非常简单的 Dockerfile。我们使用openjdk11作为基础来构建镜像。然后,我们复制生成的 jar 文件并指定在启动容器时运行 jar 文件的入口点。现在,让我们使用以下命令构建 Docker 镜像:
docker build -t abvijaykumar/bookinfo-traditional .
请随意使用您的名字标签构建 Docker 镜像。这些 Docker 镜像也可在作者的 Docker Hub 上找到,网址为hub.docker.com/u/abvijaykumar。这将构建一个镜像。我们应该能够使用以下命令查看镜像是否已创建:
docker images
让我们使用以下命令运行此镜像:
docker run -p 8080:8080 abvijaykumar/bookinfo-traditional
以下截图显示了运行上一条命令的输出:

图 10.8 – 运行 BookInformationService Spring 应用程序的控制台输出
我们可以看到它启动用了 2.107 秒。我们应该能够调用该服务。以下截图显示了调用http://localhost:8080/book-info?query=graalvm后的输出:

图 10.9 – 在容器中调用 BookInformationService Spring 应用程序的结果
现在让我们使用现代框架构建相同的服务,以了解和比较这些现代框架如何与 GraalVM 表现更好。
使用 Micronaut 构建 BookInfoService
Micronaut 是由 Grails 框架的开发者引入的全栈微服务框架。它与所有生态系统和工具集成,并依赖于编译时集成,而不是运行时集成。这使得最终应用程序运行更快,因为它们在构建时编译了所有依赖项。它通过在构建时使用注解和面向切面的编程概念来实现这一点。这于 2018 年引入。有关 Micronaut 的更多详细信息,请参阅micronaut.io/。
让我们使用 Micronaut 构建BookInfoService。要开始,我们需要安装 Micronaut 命令行。有关安装 Micronaut CLI 的详细说明,请参阅micronaut.io/download/。安装后,我们应该能够调用mn命令。现在,让我们使用mn创建我们的BookInfoService Micronaut 样板代码。以下命令创建样板代码。我们传递-b=maven标志来创建 Maven 构建:
mn create-app com.abvijay.f.mn.bookinfoservice -b=maven
| Application created at /Users/vijaykumarab/Google Drive/GraalVM-Book/Code/chapter9/mn/bookinfoservice
我们应该看到一个名为bookinfoservice的目录被创建,其中包含所有生成的样板代码。现在,让我们设置环境以指向 GraalVM。为了验证我们是否使用了正确的 GraalVM 版本,我们可以通过运行java-version来检查。以下输出显示了 GraalVM 的版本:
java -version
java version "11.0.10" 2021-01-19 LTS
Java(TM) SE Runtime Environment GraalVM EE 21.0.0.2 (build 11.0.10+8-LTS-jvmci-21.0-b06)
Java HotSpot(TM) 64-Bit Server VM GraalVM EE 21.0.0.2 (build 11.0.10+8-LTS-jvmci-21.0-b06, mixed mode, sharing)
现在让我们更新 Micronaut 代码以实现我们的逻辑。以下代码片段显示了Controller的代码,它暴露了 REST 端点:
@Controller("/bookinfo")
public class BookInfoController {
@Get("get-info")
public String getBookInfo(String query) {
BookInfoService svc = new BookInfoService();
String ret = svc.fetch(query);
return ret;
}
}
BookInfoService类与我们在前面的 Spring Boot 代码中实现的确切相同。现在,让我们通过执行以下命令来编译 Micronaut 项目:
./mvnw package
然后,我们可以通过执行以下命令来运行 Micronaut 应用:
./mvnw mn:run
以下截图显示了运行 Micronaut 应用的输出:

图 10.10 – 运行 Micronaut BookInformationService Spring 应用的控制台输出
我们可以看到,与使用 Spring 构建 BookInfoService 而不使用 GraalVM部分相比,它只用了 500 毫秒来加载 Micronaut 应用,而后者大约需要 2 秒。考虑到我们的应用既简单又小,这已经非常快了。现在让我们构建这个应用的 Docker 镜像。Micronaut 通过传递-Dpackaging=docker参数提供了一种直接使用 Maven 构建 Docker 镜像的方法。以下命令将直接生成 Docker 镜像:
mvn package -Dpackaging=docker
Micronaut 还可以生成 Dockerfile,以便我们可以自定义并单独执行。当我们将-mn:dockerfile参数传递给命令时,Dockerfiles 会在目标目录下创建。以下是我们创建的 Dockerfile:
FROM openjdk:15-alpine
WORKDIR /home/app
COPY classes /home/app/classes
COPY dependency/* /home/app/libs/
EXPOSE 8080
ENTRYPOINT ["java", "-cp", "/home/app/libs/*:/home/app/classes/", "com.abvijay.chapter9.mn.Application"]
我们可以看到,这个 Docker 镜像是基于openjdk构建的。我们仍然没有使用 GraalVM 原生镜像功能。让我们通过调用以下命令来构建这个镜像:
docker build -t abvijaykumar/bookinfo-micronaut .
docker images
现在让我们通过调用以下命令来运行这个 Docker 镜像:=
docker run -p 8080:8080 abvijaykumar/bookinfo-micronaut
以下显示了运行前面命令的输出:
__ __ _ _
| \/ (_) ___ _ __ ___ _ __ __ _ _ _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| | | | | (__| | | (_) | | | | (_| | |_| | |_
|_| |_|_|\___|_| \___/|_| |_|\__,_|\__,_|\__|
Micronaut (v2.4.1)
01:24:35.391 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 1566ms. Server Running: http://7df31221ee43:8080
我们可以看到,应用在 1.5 秒内启动,这仍然比 Spring 镜像快。我们仍然没有使用 GraalVM 原生镜像功能。现在让我们将相同的应用作为 GraalVM 原生镜像来构建。要构建原生镜像,Micronaut 支持一个 Maven 配置文件,可以通过将-Dpackaging=native-image参数传递给命令来调用。以下命令创建原生镜像:
./mvnw package -Dpackaging=native-image
docker images
现在让我们生成 Dockerfile 来了解这个镜像是如何创建的。要生成 Dockerfile,我们需要执行以下命令:
mvn mn:dockerfile -Dpackaging=docker-native
这将在目标目录下生成 Dockerfile。以下代码显示了 Dockerfile:
FROM ghcr.io/graalvm/graalvm-ce:java11-21.0.0.2 AS builder
RUN gu install native-image
WORKDIR /home/app
COPY classes /home/app/classes
COPY dependency/* /home/app/libs/
RUN native-image -H:Class=com.abvijay.chapter9.mn.Application -H:Name=application --no-fallback -cp "/home/app/libs/*:/home/app/classes/"
FROM frolvlad/alpine-glibc:alpine-3.12
RUN apk update andand apk add libstdc++
COPY --from=builder /home/app/application /app/application
EXPOSE 8080
ENTRYPOINT ["/app/application"]
我们可以看到这是一个多阶段的 Dockerfile。在第一阶段,我们正在安装原生镜像,将所有必需的应用程序文件复制到镜像中,并最终运行native-image命令来创建原生镜像。在第二阶段,我们正在复制原生镜像并提供一个入口点。
让我们运行这个镜像并看看它加载有多快。让我们执行以下命令:
docker run -p 8080:8080 bookinfoservice
以下输出显示,加载镜像仅用了 551 毫秒,这几乎是非 GraalVM Micronaut 应用程序所需时间的一半:
/app/application: /usr/lib/libstdc++.so.6: no version information available (required by /app/application)
__ __ _ _
| \/ (_) ___ _ __ ___ _ __ __ _ _ _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| | | | | (__| | | (_) | | | | (_| | |_| | |_
|_| |_|_|\___|_| \___/|_| |_|\__,_|\__,_|\__|
Micronaut (v2.4.1)
09:16:19.604 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 551ms. Server Running: http://da2bf01c90e4:8080
我们可以看到创建微服务有多容易,以及它如何无缝地与 GraalVM 工具链集成,以生成具有非常小体积和快速加载的 Docker 镜像。
Quarkus 是另一个非常流行的微服务框架。现在让我们来探索 Quarkus 并使用它构建相同的服务。
使用 Quarkus 构建 BookInfoService
Quarkus 由 Red Hat 开发,提供了与 Java 生态系统框架最复杂的集成列表。它是建立在 MicroProfile、Vert.x、Netty 和 Hibernate 标准之上的。它被构建为一个完全 Kubernetes 原生的框架。这于 2019 年推出。
现在让我们使用 Quarkus 构建 BookInfoService。Quarkus 在 code.quarkus.io 提供了一个启动代码生成器。让我们访问该网站并生成我们的代码。以下截图显示了用于生成我们的 BookInfoService 样板代码的配置。我们还包括 RESTEasy JAX-RS 来创建我们的端点:

图 10.11 – code.quarkus.io 网站的截图,用于生成样板代码
这将在 zip 文件中生成代码(我们也可以提供一个 Git 仓库,Quarkus 将自动推送代码)。现在让我们下载 zip 文件,然后使用以下命令提取和编译它:
./mvnw compile quarkus:dev.
Quarkus 最好的部分是,当我们执行此命令时,它为我们提供了一个编辑代码和测试代码而不需要重新启动服务器的方法。这有助于快速构建应用程序。现在,让我们更新 Quarkus 代码到我们的 BookInfoService 端点。
以下代码展示了端点的实现:
@Path("/bookinfo")
public class BookInfoService {
@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("/getBookInfo/{query}")
public String getBookInfo(@PathParam String query) {
String responseJson = "{}";
try {
String url = "https://www.googleapis.com/books/ v1/volumes?q=" + query + "andkey=<your google api key>";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)).build();
HttpResponse<String> response;
response = client.send(request, BodyHandlers.ofString());
responseJson = response.body();
} catch (Exception e) {
responseJson = "{'error', '" + e.getMessage() + "'}";
e.printStackTrace();
}
return responseJson;
}
}
当我们更新代码并保存时,Quarkus 会自动更新运行时。我们不需要重新启动服务器。以下截图显示了运行在 Quarkus 上的 bookservice 的输出:

BookInformationService 应用程序
图 10.12 – 调用 Quarkus 实现的 BookInformationService 应用程序的结果
现在让我们使用 Quarkus 构建 GraalVM 原生镜像。为此,我们需要编辑 pom.xml 文件并确保我们有以下配置文件:
<profiles>
<profile>
<id>native</id>
<properties>
<quarkus.package.type>native</quarkus.package.type>
</properties>
</profile>
</profiles>
Quarkus 使用 Mandrel,它是 GraalVM 的下游发行版。您可以在 developers.redhat.com/blog/2020/06/05/mandrel-a-community-distribution-of-graalvm-for-the-red-hat-build-of-quarkus/ 上了解更多关于 Mandrel 的信息。
现在我们来构建原生镜像。Quarkus 提供了一个直接的 Maven 配置文件来构建原生镜像。我们可以通过执行以下命令来创建一个原生镜像:
mvn package -Pnative
这将在目标文件夹下创建原生构建。让我们直接运行原生构建。以下是在运行原生镜像后的输出:
./bookinfoservice-1.0.0-SNAPSHOT-runner
__ ____ __ _____ ___ __ ____ ______
--/ __ \/ / / / _ | / _ \/ //_/ / / / __/
-/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2021-03-31 11:26:31,564 INFO [io.quarkus] (main) bookinfoservice 1.0.0-SNAPSHOT native (powered by Quarkus 1.13.0.Final) started in 0.015s. Listening on: http://0.0.0.0:8080
2021-03-31 11:26:31,843 INFO [io.quarkus] (main) Profile prod activated.
2021-03-31 11:26:31,843 INFO [io.quarkus] (main) Installed features: [cdi, rest-client, rest-client-jsonb, resteasy]
我们可以看到,启动应用程序仅用了0.015s。这比传统的实现快得多,后者启动需要大约 2 秒钟。
Quarkus 还创建了各种 Dockerfile 版本,我们可以在 Docker 文件夹下找到这些版本。以下截图显示了 Quarkus 自动创建的 Dockerfile 列表:

图 10.13 – Quarkus 创建的各种版本 Dockerfile 的截图
让我们快速探索这些各种类型的 Dockerfile:
-
Dockerfile.legacy-jar 和 Dockerfile.jvm:这个 Dockerfile 包含了构建带有正常 Quarkus 应用程序、JAR 和 OpenJDK 无头服务的 Docker 镜像的命令。
-
Dockerfile.native:这个 Dockerfile 构建原生镜像。
-
Dockerfile.native-distroless:这个 Dockerfile 也生成一个带有原生镜像的镜像,但使用 Google 引入的新技术来构建只包含应用程序、语言运行时而没有操作系统分发的镜像。这有助于创建一个小的镜像,并且具有更少的安全漏洞。有关 distroless 容器的更多详细信息,请参阅
github.com/GoogleContainerTools/distroless。
我们可以通过执行以下命令来创建这些各种 Docker 版本的 Docker 镜像:
docker build -f ./src/main/docker/Dockerfile.jvm -t abvijaykumar/bookinfo-quarkus-jvm .
docker build -f ./src/main/docker/Dockerfile.jvm-legacy -t abvijaykumar/bookinfo-quarkus-jvm-legacy .
docker build -f ./src/main/docker/Dockerfile.native -t abvijaykumar/bookinfo-quarkus-native .
docker build -f ./src/main/docker/Dockerfile.native-distroless -t abvijaykumar/bookinfo-quarkus-native-distroless .
要比较这些镜像的大小,让我们运行以下命令:
docker images
以下图表比较了这些镜像的大小:

图 10.14 – 比较 Docker 镜像大小的图表
在撰写本书时,使用 Quarkus 原生 distroless 镜像构建的最小足迹和执行速度最快的 GraalVM 微服务镜像。Spring 也推出了 Spring Native (spring.io/blog/2021/03/11/announcing-spring-native-beta),Oracle 有 Helidon (helidon.io/#/),它们提供了在 GraalVM 上运行的类似框架。
使用 fn 项目构建无服务器 BookInfoService
函数即服务(Function-as-a-Service,简称 FaaS),或无服务器,是另一种按需运行代码的架构模式,并利用云资源。无服务器方法在接收到请求时运行代码。代码启动、执行、处理请求,然后关闭,从而最优地利用云资源。这提供了一个高度可用、可扩展且成本最优的架构。然而,无服务器架构要求更快的启动、更快的执行和关闭。
GraalVM 原生镜像(预编译)是 FaaS 的最佳选择,因为原生镜像启动和运行速度比传统的 Java 应用程序快。GraalVM 原生镜像具有非常小的体积,它们启动速度快,并内置了虚拟机(Substrate VM)。
fn project 也是构建无服务器应用的优秀环境。Fn 支持 Go、Java、JavaScript、Python、Ruby 和 C# 的无服务器应用构建。它是一个非常简单且快速的应用开发环境,包含 fn 守护进程和 CLI,提供了构建无服务器应用的大部分脚手架。
在本节中,让我们专注于使用 fn project 构建函数 BookInfoService。请参考fnproject.io/获取安装 fn 命令行界面的详细说明。我们首先必须使用 fn start 启动 fn 守护进程服务器。fn 服务器在 Docker 中运行,你可以通过运行 docker ps 来检查。Fn 守护进程服务器运行在端口 8080。
fn 命令行还提供了一种生成样板代码的方法。现在让我们通过执行以下命令来生成项目:
fn init --runtime java book-info-service-function
Creating function at: ./book-info-service-function
Function boilerplate generated.
func.yaml created.
这将创建一个包含所有样板代码的 book-info-service-function 目录。让我们检查该目录中有什么。我们将找到 func.yaml、pom.xml 和 src 目录。
func.yml 是主要的 yaml 清单文件,其中包含实现函数的类和入口点的关键信息。让我们检查配置文件:
schema_version: 20180708
name: book-info-service-function
version: 0.0.1
runtime: java
build_image: fnproject/fn-java-fdk-build:jdk11-1.0.124
run_image: fnproject/fn-java-fdk:jre11-1.0.124
cmd: com.example.fn.HelloFunction::handleRequest
现在我们来理解前面的配置文件:
-
name:函数的名称。我们可以看到我们在fn init命令行中指定的函数名称。 -
version:此函数的版本。 -
runtime:作为运行时的 JVM。 -
build_image:用于构建 Java 代码的 Docker 镜像;在这种情况下,我们看到它是 JDK 11。 -
run_image:用作运行时的 Docker 镜像;在这种情况下,它是 JRE11。 -
cmd:这是入口点,形式为ClassName:MethodName。我们将更改cmd以指向我们的类和方法:cmd: com.abvijay.chapter9.fn.BookInfoService::getBookInfo。
在 src 文件夹中,我们将创建 com.abvijay.chapter9.fn.BookInfoService 并包含 getBookInfo() 方法。getBookInfo() 的实现与我们在本节中之前执行的其他实现相同。
以下代码展示了调用 Google API 获取书籍的函数实现:
public String getBookInfo(String query) {
String responseJson = "{}";
try {
String url = "https://www.googleapis.com/ books/v1/volumes?q=" + query + "&key=<your_google_api_key>";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)).build();
HttpResponse<String> response;
response = client.send(request, BodyHandlers.ofString());
responseJson = response.body();
} catch (Exception e) {
responseJson = "{'error', '" + e.getMessage() + "'}";
e.printStackTrace();
}
return responseJson;
}
现在让我们构建和部署这个无服务器容器到本地 Docker。函数被分组为应用程序。一个应用程序可以有多个函数。这有助于对它们进行分组和管理。因此,我们需要使用 fn create app 命令创建一个图书信息服务应用程序。以下是在执行命令后的输出:
fn create app book-info-service-app
Successfully created app: book-info-service-app
一旦创建了应用程序,我们就可以使用 fn deploy 命令来部署它。这个命令必须在创建的函数应用程序的根目录下执行。以下是在执行命令后的输出:
fn deploy --app book-info-service-app --local
Deploying book-info-service-function to app: book-info-service-app
Bumped to version 0.0.2
Building image book-info-service-function:0.0.2 .............................................................................................................
Updating function book-info-service-function using image book-info-service-function:0.0.2...
Successfully created function: book-info-service-function with book-info-service-function:0.0.2
fn deploy 命令将使用 Maven 构建代码,将其打包为 Docker 镜像,并将其部署到本地 Docker 运行时。fn 还可以直接用于部署到云或 k8s 集群。
现在让我们使用 docker images 命令来检查我们的镜像是否已构建:
docker images
我们还可以使用 fn inspect 来获取有关函数的所有详细信息。这有助于发现服务。以下是在执行命令后的输出:
fn inspect function book-info-service-app book-info-service-function
{
"annotations": {
"fnproject.io/fn/invokeEndpoint":
"http://localhost:8080/invoke/ 01F29E8SXKNG8G00GZJ0000002"
},
"app_id": "01F29E183WNG8G00GZJ0000001",
"created_at": "2021-04-02T14:02:25.587Z",
"id": "01F29E8SXKNG8G00GZJ0000002",
"idle_timeout": 30,
"image": "book-info-service-function:0.0.2",
"memory": 128,
"name": "book-info-service-function",
"timeout": 30,
"updated_at": "2021-04-02T14:02:25.587Z"
}
现在让我们调用这个服务。由于我们的函数期望一个数字类型的输入参数,我们可以使用 echo 命令传递它,并将输出通过管道传递给 fn invoke 来调用我们的函数:
echo -n 'java' | fn invoke book-info-service-app book-info-service-function
{
"kind": "books#volumes",
"totalItems": 1941,
"items":
{
"kind": "books#volume",
"id": "Q3_QDwAAQBAJ",
"etag": "0Hl3HzInpzY",
"selfLink": "https://www.googleapis.com/ books/v1/volumes/Q3_QDwAAQBAJ",
"volumeInfo": {
"title": "Java Performance",
…
我们可以看到函数的执行过程和 Google API 的输出(前面的输出是部分内容,以节省空间)。现在让我们在 GraalVM 上运行相同的逻辑。
GraalVM 的基础镜像不同。我们使用 fnproject/fn-java-native-init 作为基础,并使用它初始化我们的 fn 项目。以下是生成基于 Graal 本地图像的 fn 项目的输出:
fn init --init-image fnproject/fn-java-native-init book-info-service-function-graal
Creating function at: ./book-info-service-function-graal
Running init-image: fnproject/fn-java-native-init
Executing docker command: run --rm -e FN_FUNCTION_NAME=book-info-service-function-graal fnproject/fn-java-native-init
func.yaml created.
这将生成一个 Dockerfile。接下来是 Dockerfile 代码,您可以在项目目录(book-info-service-function-graal)下找到它。这个 fn 配置工作方式不同。它也生成一个 Dockerfile,包含所有必要的 Docker 构建命令。这是一个多阶段 Docker 构建文件。让我们检查这个 Dockerfile:
![Figure 10.15 – Dockerfile generated by fn
![img/Figure_10.15_B16878.jpg
图 10.15 – 由 fn 生成的 Dockerfile
让我们理解这个 Dockerfile:
-
fnproject/fn-java-fdk-build。 -
/function。 -
第 19–23 行:然后,配置 Maven 环境。
-
使用
fnproject/fn-java-native作为基础镜像,配置 GraalVM 并编译 fn 运行时。这是一个非常重要的步骤,因为这使我们的无服务器运行时更快,并且占用更小的空间。 -
以
busybox:glibc(这是 Linux+glibc 的最小版本)为基础镜像。 -
func.yml,在这种构建无服务器镜像的方式中,没有任何信息。fn 将使用 Dockerfile 来执行构建(以及 Maven)并将镜像部署到仓库。
我们需要将第 48 行更改为指向我们的类。让我们用以下内容替换它:
CMD ["com.abvijay.chapter9.fn.BookInfoService::BookInfoService"]
我们还需要更改另一个重要的配置文件,即位于src/main/conf下的reflection.json。此 JSON 文件包含有关类名和方法的信息的清单。它被原生镜像构建器用于解决我们通过动态调用我们的函数所进行的反射。请参阅第五章中的[构建原生镜像]部分,Graal Ahead-of-Time Compiler 和 Native Image。
现在,让我们创建一个 fn 应用,并使用fn create app命令部署此应用。以下是执行命令后的输出:
fn create app book-info-service-app-graal
Successfully created app: book-info-service-app-graal
我们可以使用fn deploy –app book-info-service-app-graal命令构建原生镜像并部署它,并且我们可以通过调用echo -n 'java' | fn invoke book-info-service-app-graal book-info-service-function来执行方法。检查 Docker 镜像,我们会看到 Java 镜像的大小为 238 MB,而 GraalVM 镜像的大小仅为 41 MB。这比传统的 Java 应用程序小 10 倍。我们可以计时函数调用,并且我们可以看到原生镜像运行得更快(高达 30%)。
docker images
无服务器是最佳解决方案,因为它适用于快速且无状态的服务,它不占用任何资源,我们也不需要一直保持其运行。
在本节中,我们探讨了各种框架实现和优化图像的方法。
摘要
恭喜您达到这一阶段!在本章中,我们探讨了微服务架构的构建方式。为了理解架构思考过程,我们选择了一个简单的案例研究,并探讨了它如何作为微服务集合部署在 Kubernetes 上。然后,我们探讨了各种微服务框架,并在每个框架上构建了一个服务,以欣赏 GraalVM 为云原生架构带来的好处。
在阅读本章后,您应该已经对如何使用 GraalVM 作为运行时构建基于微服务的云原生应用程序有了很好的理解。本章为 Java 开发者快速开始在微服务框架(Quarkus、Spring、Micronaut)之一上构建应用程序提供了一个良好的起点。本章提供的源代码(在 Git 中)也将提供微服务在 GraalVM 上的良好参考实现。
问题
-
什么是微服务?
-
微服务架构的优势是什么?
-
为什么 GraalVM 是微服务的理想应用程序运行时?
进一步阅读
-
微服务架构 (
microservices.io/) -
Micronaut (
microprofile.io/) -
Quarkus (
quarkus.io/) -
Spring Boot (
spring.io/) -
Spring Native (
docs.spring.io/spring-native/docs/current/reference/htmlsingle/)
第十四章:评估
本节包含所有章节的问题答案。
第一章 – Java 虚拟机的发展历程
-
Java 代码编译成字节码。JVM 使用解释器将字节码转换为机器语言,并使用即时编译器编译最常用的代码片段(热点)。这种方法帮助 Java 实现“一次编写,到处运行”,因此程序员不需要编写特定于机器的代码。
-
类加载器子系统负责加载类。它不仅找到类,还验证和解析类。
-
JVM 有五个内存区域:
a. 方法:一个共享区域,其中所有类级别的数据都存储在 JVM 级别
b. 堆:所有实例变量和对象存储在 JVM 级别(跨线程共享)
c. 栈:每个线程的运行时栈,用于存储方法作用域内的局部变量,以及操作数和帧数据
d. 注册表:具有当前执行指令地址的 PC 寄存器(每个线程)
e. 原生方法栈:用于调用原生方法的每个线程的原生方法信息
第二章 – JIT、Hotspot 和 GraalJIT
-
代码缓存是 JVM 内部的一个特殊内存区域,用于存储编译后的代码。代码由即时编译器编译并存储在代码缓存中。如果一个方法被编译并发现存在于代码缓存中,JVM 将使用该代码来运行,而不是解释方法代码。有关更多详细信息,请参阅代码缓存部分。
-
可以使用以下标志来更改代码缓存大小,以进行微调。有关更多详细信息,请参阅代码缓存部分:
a.
-XX:InitialCodeCacheSize– 代码缓存的初始大小。默认大小是 160 KB(大小根据 JVM 版本而变化)b.
-XX:ReservedCodeCacheSize– 这是代码缓存可以增长到的最大大小。默认大小是 32/48 MB。当代码缓存达到这个限制时,JVM 将抛出一个警告,“代码缓存已满。编译器已被禁用。” JVM 提供了UseCodeCacheFlushing选项,当代码缓存满时可以刷新代码缓存。当编译的代码不够热(计数器小于编译器阈值)时,也会刷新代码缓存。c.
-XX:CodeCacheExpansionSize– 这是扩展大小。当它扩大时,其默认值是 32/64 KB。 -
编译器阈值是用于决定代码何时“热”的因子。当代码达到编译器阈值时,JVM 将在编译线程上启动即时编译(C1 或 C2)。有关更多详细信息,请参阅编译器阈值部分。
-
有时,代码在运行长时间循环时可能会变得“热”。在这种情况下,JVM 将编译该代码并执行 OSR。有关更多详细信息以及 JVM 如何执行 OSR 的详细流程图,请参阅栈上替换部分。
-
在 JVM 中,有一个解释器和两种类型的编译器——C1 和 C2。用户可以指定任何特定的编译器来优化代码。默认情况下,JVM 执行分层编译,这是基于各种编译器阈值的 C1 和 C2 的组合。共有五个层级:
a. 解释型代码(级别 0)
b. 简单 C1 编译代码(级别 1)
c. 有限 C1 编译代码(级别 2)
d. 完整 C1 编译代码(级别 3)
e. C2 编译代码(级别 4)
JVM 遵循三种主要模式:
a. 正常流程
b. C2 繁忙
c. 简单代码
-
请参阅分层编译部分以获取更多详细信息。
-
内联是 JIT 编译器使用的关键优化技术之一。基于代码分析,JIT 识别出可以内联的方法以避免方法调用。方法调用成本较高,因为它执行跳转并创建栈帧。
-
单态分发是另一种用于识别多态实现的具体实现的优化技术。JIT 分析代码,识别具体实现,并围绕该实现优化代码。请参阅单态、双态和巨态分发部分以获取更多详细信息。
-
循环展开是 JIT 执行的最有效的优化之一,通过在循环体中内联代码、添加额外代码并减少循环必须迭代的次数来实现。请参阅循环优化——循环展开部分以获取更多详细信息及示例。
-
逃逸分析是 JIT 分析器执行的一种优化技术,用于识别变量的分配和作用域,并基于变量的作用域做出避免堆分配的决定,并用栈分配替换。这是 JIT 分析器执行的最先进的分析之一。请参阅逃逸分析部分以获取更多详细信息。
-
当 JIT 在优化和编译代码时所做的任何乐观假设无效时,JIT 将执行去优化。JIT 将编译代码设置为非可进入状态,并回退到解释器。
-
JVMCI 代表Java 虚拟机编译器接口。此接口在 Java 9 中添加到 JDK 中。JVMCI 提供了一个 API 来扩展 JVM 并构建自定义编译器。Graal JIT 是 JVMCI 的一个实现。请参阅Graal JIT 和 JVM 编译器接口(JVMCI)部分以获取更多详细信息。
第三章 - Graal VM 架构
-
GraalVM 有两种版本——社区版和企业版。请参阅查看 GraalVM 版本部分以获取更多详细信息。
-
JVMCI 代表Java 虚拟机编译器接口。Java 9 及以上版本提供了一种实现自定义 JIT 编译器的方法。JVMCI 提供了一个 API 来实现这些自定义编译器,并提供了对 JVM 对象和代码缓存的访问。Graal JIT 是 JVMCI 的一个实现。请参阅Java 虚拟机编译器接口(JVMCI)部分以获取更多详细信息。
-
Graal JIT 替代了 C2 JIT 编译器。Graal JIT 完全用 Java 从头编写,但使用了 C2 编译器的强化逻辑和最佳实践。Graal JIT 实现了比 C2 JIT 更好的优化策略,使其成为 Java 最好的 JIT 编译器。Graal JIT 还可以用于编译其他语言,这些语言被转换为中间表示,以便使用高级优化策略。有关详细信息,请参阅 Graal 编译器和工具 部分。
-
Graal JIT 需要相当长的时间来预热、配置文件和优化代码。在某些用例中,这可能不合适(如无服务器或容器)。对于此类情况,Graal 提供了 AOT 编译,将代码直接编译成本地图像。
-
Graal AOT 优化更相关于静态代码分析,但现在它已经有了代码的运行时配置文件来应用任何高级优化。配置文件引导优化(PGO)提供了一种方法,通过添加工具来编译代码,生成运行时配置文件,并使用该配置文件重新编译代码以生成最优化本机图像。有关详细信息,请参阅 SubstrateVM(Graal AOT、本地图像) 部分。
-
Truffle 框架建立在 Graal 之上,以支持非 JVM 语言在 Graal JVM 上运行。Truffle 提供了 Truffle 语言实现 API 以及各种其他多语言 API,以提供一个非常复杂的编程环境,其中多种语言的代码可以嵌入并交互。有关详细信息,请参阅 Truffle 部分。
-
SubstrateVM 是一个可嵌入的虚拟机,可以与 Graal AOT 编译器编译的本地图像一起打包。有关详细信息,请参阅 SubstrateVM(Graal AOT 和本地图像) 部分。
-
客户端访问上下文是一个对象,由宿主语言(如 Java)使用,以提供对客户端语言(如 JavaScript)以及各种操作系统资源(如文件系统、I/O 和线程)的访问。有关详细信息,请参阅 安全 部分。
-
GraalVM 提供了最先进的 JIT 编译,非常适合涉及高吞吐量的长时间运行过程。GraalVM AOT 编译器以及 SubstrateVM 提供了云原生微服务实现的最小和最快的运行时。结合 PGO,它生成在云上运行的优化代码。有关详细信息,请参阅 GraalVM 微服务架构概述 部分。
第四章 – Graal 即时编译器
-
Graal JIT 编译可以分为两个阶段:前端和后端。
前端阶段是平台无关的编译,其中代码被转换为一种平台无关的中间表示,称为 高级中间表示(HIR),通过 Graal 图表示。这种 HIR 在三个级别上进行优化:高、中、低。
后端阶段是更依赖于平台的编译,其中在机器代码级别创建并优化了低级中间表示法(LIR)。这些优化依赖于平台。
请参阅Graal JIT 编译器管道和分层优化部分以获取更多详细信息。
-
中间表示法(IRs)是编译器设计中最重要的数据结构之一。中间表示法提供了一个图,帮助编译器理解代码的结构,识别机会,并执行优化。请参阅Graal 中间表示法部分以获取更多详细信息。
-
静态单赋值(SSA)是中间表示法中使用的一种形式,其中每个变量只赋值一次,任何时间值发生变化时,都会使用一个新的变量。每个变量在使用之前都会被声明。这有助于我们跟踪变量和值,并有助于使用图更好地优化代码。请参阅Graal 中间表示法部分以获取更多详细信息。
-
投机优化是一种编译器优化技术,通过投机执行各种代码优化。投机是基于对代码进行剖析所做的假设。根据这些假设在代码上执行优化。当这些假设在运行时被证明是错误的时候,会执行去优化。这有助于优化代码的焦点部分,而不是整个代码,这可能会减慢运行时。请参阅Graal 编译器优化部分以获取更多详细信息。
-
逃逸分析是一种优化技术,它识别对象的作用域和用法,并决定对象在堆、栈或寄存器上的分配。这对内存使用和性能有重大影响。逃逸分析在方法级别执行,而部分逃逸分析则对代码进行更深入的分析,以跟踪不仅限于方法级别作用域的对象,还包括控制块级别的对象。这有助于进一步优化代码。请参阅部分逃逸分析部分以获取更多详细信息。
第五章 - Graal 即时编译器和原生图像
-
GraalVM 附带一个名为 Native Image builder 的工具,即
native-image。这可以用来编译并创建原生图像。请参阅构建原生图像部分以获取更多详细信息。 -
当 Native Image builder 在编译代码之前执行时,会进行指针分析以理解应用程序代码访问的所有依赖类和方法。它使用这些信息来优化原生图像,只将所需的代码构建到图像中。这提供了更快的执行速度和更小的图像。请参阅构建原生图像部分以获取更多详细信息。
-
原生图像构建器执行区域分析,以便在启动之前将类初始化到堆中,从而使原生图像的启动更快。请参阅构建原生图像部分以获取更多详细信息。
-
原生图像构建器将垃圾收集器(GC)代码与原生图像一起打包。原生图像中可以启用两种类型的 GC。串行 GC 是默认的 GC,在社区和商业版中均可用。G1 执行更高级的垃圾收集,仅在商业版中可用。请参阅原生图像内存管理配置部分以获取更多详细信息。
-
与 JIT 编译器不同,原生图像构建器只能执行静态代码分析,JIT 编译器可以在运行时对代码进行性能分析并在运行时优化代码。PGO 将运行时性能分析信息带到原生图像中,以进行进一步优化。请参阅性能引导优化(PGO)部分以获取更多详细信息。
-
由于原生图像是在构建之前构建的,原生图像构建器需要在构建时加载所有类。因此,原生图像在支持动态功能(如反射和 JNI)方面存在限制。然而,GraalVM 的原生图像构建器提供了在构建时传递动态资源信息的方法。请参阅原生图像配置和Graal AOT(原生图像)限制部分以获取更多详细信息。
第六章 – 松露 – 概述
-
专业化是一种关键优化,有助于识别变量的特定类型。在动态类型语言中,变量的类型在代码中未声明。解释器开始假设通用类型,并根据运行时性能分析推测变量的类型。请参阅Truffle 解释器/编译器管道部分以获取更多详细信息。
-
当 Truffle 对节点的一个特定类型进行推测时,节点会动态重写,Truffle AST 提供了一种方法,在将节点提交给 Graal 进行进一步优化执行之前,优化 AST。请参阅Truffle 解释器/编译器管道部分以获取更多详细信息。
-
当 Truffle 发现 AST 没有被重写时,它假定 AST 已经稳定。然后,在进行了激进的常量折叠、内联和逃逸分析之后,将代码编译为客语言的机器代码。这被称为部分评估。请参阅Truffle 解释器/编译器管道部分的部分评估以获取更多详细信息。
-
松露提供了一种作为注解生成器实现的领域特定语言。这有助于客语言开发者编写更小的代码,并专注于逻辑,而不是样板代码。请参阅Truffle DSL部分以获取更多详细信息。
-
框架是 Truffle 类,它提供了在当前命名空间中读取和存储数据的接口。请参阅Truffle 框架管理和局部变量部分以获取更多细节。
-
Truffle 定义了一个动态对象模型,为各种客语言实现提供了一个标准接口和框架,以便以标准方式定义和交换数据。请参阅Truffle 互操作性部分的动态对象模型以获取更多细节。
第七章 – GraalVM 多语言 – JavaScript 和 Node.js
-
Polyglot是 JavaScript 中用于运行其他语言代码的对象。我们使用eval()方法来运行代码。请参阅JavaScript 互操作性部分,了解如何使用此对象运行代码的更多细节。 -
Context对象提供了多语言上下文,以便在宿主语言中运行客语言代码。多语言上下文表示所有已安装和允许的语言的全局运行时状态。请参阅Java 中嵌入 JavaScript 代码部分,了解如何使用此对象运行代码的更多细节。 -
Context对象有助于提供细粒度的访问控制。访问控制可以通过ContextBuilder进行控制。请参阅Java 中嵌入 JavaScript 代码部分,了解如何使用此对象运行代码的更多细节。 -
GraalVM 提供了一个原生图像构建器选项,用于构建具有多种嵌入式语言的程序的原生图像。语言标志用于让原生图像构建器知道应用程序中使用了哪些语言。此标志也可以在
native-image属性文件中指定。请参阅本章的多语言原生图像部分以了解更多。有关原生图像的更多详细信息,请参阅第五章,Graal 即时编译器和原生图像。 -
binding对象在 Java 和 JavaScript 之间充当中间层,用于访问两种语言之间的方法、变量和对象。请参阅绑定部分,了解更多关于绑定对象及其作为语言之间中间层的用法。
第八章 – GraalVM 多语言 – Java 在 Truffle、Python 和 R 上
-
Java 在 Truffle 是运行 Java 程序在 Truffle 框架之上的新方法。Java 在 Truffle 提供了一个完全基于 Java 构建的解释器,并在与其他 Truffle 语言相同的内存空间中运行。这是在 GraalVM 版本 21 中引入的。更多详细信息请参阅理解 Espresso(Java 在 Truffle 上)部分。
-
在 Truffle 上运行的 Java 提供了一个隔离层,有助于运行不受信任的代码和用较旧版本的 JDK 编写的代码,并提供热插拔和其他高级功能。要了解更多关于在 Truffle 上使用 Java 的优势,请参阅为什么我们需要在 Java 上运行 Java?部分以获取更多详细信息。
-
在 Truffle 上,
Polyglot.cast()方法用于将动态语言导出或返回的数据进行类型转换。请参阅 探索 Espresso 与其他 Truffle 语言之间的互操作性 部分以获取更多细节和代码示例。 -
SST 代表 Simple Syntax Tree,而 ST 代表 Scope Tree。Python 在将它们转换为 AST 中间表示之前生成这些中间表示。Python 使用 ANTLR 解析器和缓存来完成此操作,并加快了解析速度。请参阅 理解 Graalpython 编译器和解释器管道 部分以获取更多细节。
-
.pyc文件是 Python 在解析 Python 代码并生成 SST 和 ST 表示之后创建的缓存。这有助于加快下次加载 Python 模块时的解析速度。Python 会自动保持此缓存的有效性。请参阅 理解 Graalpython 编译器和解释器管道 部分以获取更多细节。 -
polyglot.import_value()用于从其他动态语言导入定义,而polyglot.export_value()用于将 Python 定义导出到其他语言。polyglot.eval()用于执行其他语言代码。请参阅 探索 Python 与其他动态语言之间的互操作性 部分以获取更详细的解释和示例代码。 -
在 R 中,我们使用
import()函数从其他语言导入定义。请参阅 探索 R 的互操作性 部分以获取更多细节。 -
我们使用
java.type('classname')来加载 Java 类并与它交互。此函数提供了类,我们可以使用new()函数来创建对象的实例。请参阅 探索 R 的互操作性 部分以获取更多细节和示例代码。
第九章 – GraalVM Polyglot – LLVM、Ruby 和 WASM
-
Sulong 是一个用 Java 编写的 LLVM 解释器,它内部使用 Truffle 语言实现框架。这使得所有可以生成 LLVM IR 的语言编译器都可以直接在 GraalVM 上运行。请参阅 理解 LLVM – (Sulong) Truffle 接口 部分以获取更多细节。
-
GraalVM 企业版提供了一个管理的 LLVM 环境。执行管理的模式提供了一个安全的运行时环境,它通过额外的安全性保证能够捕获非法指针访问和访问超出边界的数组。
TruffleRuby 解释器与 LLVM 解释器交互以实现 C 扩展。这也扩展了使用其他 LLVM 语言(如 Rust 和 Swift)作为 Ruby 扩展运行的可能性。请参阅 理解 TruffleRuby 解释器/编译器管道 部分以获取更多细节。
-
WASM 是可以在现代网络浏览器上运行的二进制代码。它具有非常小的体积,并且比 JavaScript 执行速度快得多。请参阅 理解 WASM 部分以获取更多细节。
Emscripten 或
emcc是生成 WASM 二进制图像(.wasm)文件的编译器。请参阅“安装和运行 GraalWasm”部分以获取更多详细信息。
第十章 – 使用 GraalVM 的微服务架构
-
微服务是一种将大型应用程序分解为更小、更易于管理和自包含的组件的架构模式,这些组件通过称为服务的标准接口公开其功能。请参阅“微服务架构概述”部分以获取更多详细信息。
-
微服务架构模式帮助我们构建一个可扩展、可管理和松散耦合的应用程序。这对于构建云原生应用以充分利用云基础设施和服务非常重要。请参阅“微服务架构概述”部分以获取更多详细信息。
-
GraalVM 提供了一个具有小内存占用的高性能运行时环境,这对于构建可扩展的云原生应用至关重要。请参阅第三章“Graal VM 架构”中的“审查现代架构要求”部分以及本章的“理解 GraalVM 如何帮助构建微服务架构”部分以获取更多详细信息。

订阅我们的在线数字图书馆,全面访问超过 7,000 本书籍和视频,以及行业领先的工具,帮助你规划个人发展并推进职业生涯。更多信息,请访问我们的网站。
第十五章:为什么订阅?
-
使用来自超过 4,000 位行业专业人士的实用电子书和视频,花更少的时间学习,更多的时间编码
-
通过为你量身定制的 Skill Plans 提高你的学习效果
-
每月免费获得一本电子书或视频
-
完全可搜索,便于轻松访问关键信息
-
复制粘贴、打印和收藏内容
你知道 Packt 为每本书都提供电子书版本,包括 PDF 和 ePub 文件吗?你可以在packt.com升级到电子书版本,作为印刷书客户,你有权获得电子书副本的折扣。通过customercare@packtpub.com联系我们获取更多详情。
在www.packt.com,你还可以阅读一系列免费技术文章,订阅各种免费通讯,并享受 Packt 书籍和电子书的独家折扣和优惠。
你可能还会喜欢以下书籍
如果你喜欢这本书,你可能对 Packt 的以下其他书籍感兴趣:
Jakarta EE Cookbook
Elder Moraes
ISBN: 978-1-83864-288-4
-
探索 Jakarta EE 的最新特性和 API 规范,并了解它们的益处
-
使用 Jakarta EE 8 和 Eclipse MicroProfile 构建和部署微服务
-
使用 JAX-RS、JSON-P 和 JSON-B API 为各种企业场景构建健壮的 RESTful Web 服务
Microservices with Spring Boot and Spring Cloud,第 2 版
马格努斯·拉尔森
ISBN: 978-1-80107-297-7
-
使用本全面更新的指南构建云原生生产就绪的微服务
-
了解构建大规模微服务架构的挑战
-
学习如何结合 Spring Cloud、Kubernetes 和 Istio 获得最佳效果
Packt 正在寻找像你这样的作者
如果你对成为 Packt 的作者感兴趣,请访问authors.packtpub.com并今天申请。我们与成千上万的开发者和技术专业人士合作,就像你一样,帮助他们将见解分享给全球技术社区。你可以提交一般申请,申请我们正在招募作者的特定热门话题,或者提交你自己的想法。
留下评论 - 让其他读者了解你的想法
请通过在购买书籍的网站上留下评论的方式,与大家分享您对这本书的看法。如果您在亚马逊购买了这本书,请在本书的亚马逊页面留下一个诚实的评论。这对其他潜在读者来说至关重要,他们可以通过阅读您的客观意见来做出购买决定,我们也可以了解客户对我们产品的看法,我们的作者也可以看到他们对与 Packt 合作创作的书籍的反馈。这只需您几分钟的时间,但对其他潜在客户、我们的作者和 Packt 来说都非常有价值。谢谢!


浙公网安备 33010602011771号