vlambda博客
学习文章列表

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

Chapter 5: Graal Ahead-of-Time Compiler and Native Image

Graal 提前编译有助于构建比传统 Java 应用程序启动更快、占用空间更小的原生镜像。原生镜像对于现代云原生部署至关重要。 GraalVM 捆绑了一个名为 native-image 的工具,用于提前编译并生成原生图像。

native-image 将代码编译成本机可执行/本机二进制文件,无需虚拟机即可独立运行。可执行文件包括所有类、依赖项、库,更重要的是,包括所有虚拟机功能,例如内存管理、线程管理等。虚拟机功能被打包为一个名为 Substrate VM 的运行时。我们在 第 3 章中简要介绍了 Substrate VM,< em>GraalVM 架构,位于 Substrate VM(Graal AOT 和本机映像) 部分。在本章中,我们将对原生图像有更深入的了解。我们将通过示例学习如何构建、运行和优化原生镜像。

原生镜像只能执行静态代码优化,不具备即时编译器执行的运行时优化的优势。我们将探索配置文件引导优化,这是一种可用于通过使用运行时分析数据来优化本机图像的技术。

在本章中,我们将介绍以下主题:

  • Understanding how to build and run native images
  • Understanding the architecture of a native image, and how the compilation works
  • Exploring various tools, compilers, and runtime configurations to analyze and optimize the way native images are built and executed
  • Understanding how to optimize native images using Profile-Guided Optimization (PGO)
  • Understanding the limitations of native images, and how to overcome these limitations
  • Understanding how memory is managed by native images

到本章结束时,你将对 Graal 提前编译有一个清晰的认识,以及构建和优化原生镜像的实践经验。

Technical requirements

我们将使用以下工具和示例代码来探索和理解 Graal 提前编译:

Building native images

在本节中,我们将使用 Graal 原生图像构建器 (native-image) 构建原生图像。 让我们首先安装 Native Image builder。

native-image 可以使用 GraalVM Updater 使用以下命令安装:

gu 安装本机映像

该工具直接安装在GRAALVM_HOME的/bin文件夹中。

现在让我们从 第 4 章中的 Graal 编译器配置部分创建 FibonacciCalculator 的原生图像>Graal 即时编译器

要创建原生图像,请编译 Java 文件并运行 native-image FibonacciCalculator –no-fallback -noserver。以下屏幕截图显示了运行命令后的输出:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.1 – FibonacciCalculator – 生成原生图像控制台输出

Native Image 编译需要时间,因为它必须执行大量静态代码分析来优化生成的图像。下图显示了 Native Image builder 执行的提前编译流程:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.2 – Native Image 管道流程

让我们尝试更好地理解这张图片:

  • The ahead-of-time compiler loads all the application code and dependency libraries and classes and packages them along with the Java Development Kit classes and Substrate VM classes.
  • Substrate VM has all the virtual machine functionality that is required to run the application. This includes memory management, garbage collection, thread management, scheduling, and so on.
  • The compiler then performs the following optimization on the code before building the native image:

    一个。 指向分析:仅识别使用和调用的类和方法,并消除所有从未使用或调用的代码。例如,我们的代码可能不会做很多数学运算或字符串运算。获取完整的 JDK 并将其转换为机器代码是没有意义的。相反,只选择使用的类/方法。指向分析假设应用程序中曾经使用过的所有字节码都是可用的。这意味着无法在运行时加载类。甚至不直接支持使用反射对类结构进行运行时修改。但是,如果应用程序使用反射,我们可以在 JSON 中创建一个元文件来配置反射(META-INF/native-image/reflect-config.json)。还可以配置其他动态特性,例如 JNI、代理等。配置文件需要在 CLASSPATH 中,然后编译器会负责将这些功能包含在最终的原生映像中。

    湾。 初始化:初始化类并执行堆分配。

    C。 区域分析和堆快照:创建堆快照,以便可以创建堆映像并将其构建到本机映像中,以加快加载速度。堆快照收集运行时可访问的所有对象。这包括类初始化、初始化 static 和 static final 字段、enum 常量、java.lang.Class 对象、等等。

  • The final native image has the code section, where the final optimized binary code is placed, and in the data section of the executable, the heap image is written. This will help load the native image quickly.

以下是在构建时和运行时如何进行点到分析和区域分析的更高级别流程:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.3 – 原生图像管道 – 指向分析和区域分析

让我们详细了解这张图:

  • At build time, the points-to analysis scans through the application code, dependencies, and JDK to find reachable code.
  • The region analysis captures the heap region metadata, which includes region mappings and the region entry/exit. Region analysis also uses the reachable code to identify which static elements need to be initialized.
  • The code is generated with the reachable code and the region metadata.
  • At runtime, the heap allocator allocates ahead of time using the region mappings and the region manager handles the entry and exit.

现在让我们通过从命令行发出 ./fibonaccicalculator 来运行 本机图像。下面是执行原生镜像的截图:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.4 – FibonacciCalculator – 运行 fibonaccicalculator 本机图像控制台输出

提前编译的最大缺点之一是编译器永远无法分析运行时以优化代码,这在即时编译中非常好。为了将两全其美,我们可以使用 PGO 技术。我们在第 3 章中简要介绍了 PGO,GraalVM 架构。让我们看看它的实际效果并更深入地理解它。

Analyzing the native image with GraalVM Dashboard

为了更深入地了解 point-to 分析和区域分析的工作原理,我们可以使用 GraalVM Dashboard。在本节中,我们将在构建原生镜像时创建转储,并使用 GraalVM 可视化原生镜像构建器执行指向分析和区域分析。

第 4 章调试和监控应用程序部分中,Graal Just-In-Time编译器,我们简要介绍了 GraalVM Dashboard。 GraalVM Dashboard 是一个非常强大的工具,专门用于原生图像。在本节中,我们将生成 FibonnacciCalculator 示例的仪表板转储,并探索如何使用 GraalVM 仪表板来深入了解本机图像。

要生成仪表板转储,我们必须使用 -H:DashboardDump=<文件名> 标志。对于我们的 FibonacciCalculator,我们使用以下命令:

native-image -H:DashboardDump=dashboard -H:DashboardAll FibonacciCalculator

以下屏幕截图显示了此命令生成的输出。该命令创建了一个 dashboard.bgv 文件:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.5 – FibonacciCalculator – 生成仪表板转储控制台输出

我们还使用 -H:DashboardAll 标志来转储所有参数。以下是我们可以使用的替代标志:

  • -H:+DashboardHeap: This flag only dumps the image heap.
  • -H:+DashboardCode: This flag generates the code size, broken down by method.
  • -H:+DashboardPointsTo: This flag creates a dump of the points-to analysis that the Native Image builder has performed.

现在让我们加载这个 dashboard.bgv,并分析结果。我们需要将 dashboard.bgv 文件上传到 GraalVM Dashboard。打开浏览器并转到 https://www.graalvm.org/docs/tools/dashboard/

然后我们应该看到以下屏幕:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.6 – GraalVM Dashboard 主页

点击左上角的+加载数据按钮。您将看到一个对话框,如下图所示:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.7 – 上传仪表板转储文件窗口

点击 Select File 按钮并指向我们生成的 dashboard.bgv 文件。您会立即看到仪表板,如图5.8所示。您会在左侧找到两个生成的报告——代码大小分解和堆大小分解。

Understanding the code size breakdown report

代码大小细分报告提供了分类成块的各种类的代码大小。块的大小代表代码的大小。下图显示了当我们在上一节中生成的 dashboard.bgv 的左侧窗格中选择 Code Size Breakdown 选项时的初始仪表板屏幕。通过将鼠标悬停在块上,我们可以通过方法获得更清晰的尺寸细分。我们可以双击这些块来深入挖掘:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.8 – 代码大小分解仪表板

下面的 屏幕截图显示了我们在 双击 FibonacciCalculator 块时看到的报告。再次,我们可以双击调用图:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.9 – 代码大小细分 – 详细报告

以下屏幕截图 显示了调用图。这有助于我们了解原生图像构建器执行的指向分析。如果我们识别出任何未使用的类或方法的依赖关系,这可以用来识别优化源代码的机会:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.10 – 代码点依赖报告

现在让我们看看堆大小分解。

Heap size breakdown

堆大小细分提供了对堆分配的详细了解,还提供了对堆的深入了解。我们可以双击这些块来了解这些堆分配。以下屏幕截图显示了 FibonacciCalculator 的堆大小细分报告:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.11 – 堆大小细分仪表板

在本节中,我们研究了如何使用 GraalVM Dashboard 分析原生图像。现在让我们看看如何使用 PGO 优化我们的原生图像。

Understanding PGO

使用 PGO,我们可以运行本机映像,并选择生成运行时配置文件。 JVM 会创建一个配置文件 .iprof,该文件可用于重新编译本机映像,以进一步优化它。下图(回想一下 第 3 章, GraalVM 架构) 展示了 PGO 的工作原理:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.12 – Native Image – 配置文件引导的优化流水线流程

上图展示了使用PGO的原生镜像编译流水线。让我们更好地理解这个流程:

  • The initial native image is instrumented to create a profile by passing the –pgo-instrument flag argument, while bundling the native image. This will generate a native image with instrumentation code.
  • When we run the native image with several inputs, a profile is created by the native image. This profile is a file generated in the same directory with the .iprof extension.
  • Once we have run through all the use cases, to ensure that the profile created covers all the paths. We can then rebuild the native image by passing the .iprof file as a parameter along with the --pgo argument.
  • This will generate the optimized native image.

现在让我们为 FibonacciCalculator 类构建一个优化的原生映像。让我们首先通过运行 following 命令创建一个插桩的原生镜像:

java -Dgraal.PGOInstrument=fibonaccicalculator.iprof -Djvmci.CompilerIdleDelay-0 FibonacciCalculator

此命令将使用配置文件信息构建 本机图像。以下屏幕截图显示了构建原生镜像的输出:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.13 – FibonacciCalculator – 生成 PGO 配置文件控制台输出

这将在当前目录中生成 fibonaccicalculator.iprof 文件。现在让我们使用此配置文件使用以下命令重建我们的本机映像:

native-image –pgo=fibonaccicalculator.iprof FibonacciCalculator

这将使用最佳可执行文件重建本机映像。以下屏幕截图显示了我们使用配置文件构建本机映像时的输出:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.14 – FibonacciCalculator – 生成配置文件引导的优化本机图像控制台输出

现在让我们执行优化文件。下面的 截图显示了我们运行优化后的原生镜像时的输出结果:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.15 – FibonacciCalculator – 运行 PGO 图像控制台输出

如您所见,它比原始原生图像快得多。现在让我们比较一下这些值。下图显示了压缩:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.16 – FibonacciCalculator – 原生图像与 PGO 原生图像比较

如您所见,PGO 的性能比 更快更好。

虽然这一切都很好,但如果您将其与 JIT 进行比较,我们会发现本机映像的性能并不那么好。让我们将其与 JIT(Graal 和 Java HotSpot)进行比较。下图显示了比较:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.17 – FibonacciCalculator – Graal JIT 与 Java HotSpot 与原生镜像与 PGO 原生镜像的比较

这突出了一个关键点,即 原生图像并不总是最佳的。在这种情况下,肯定不是,因为我们正在对我们正在构建的大型数组进行堆分配。这直接会影响性能。这是作为开发人员优化代码的重要领域之一。原生镜像使用串行 GC,因此将原生镜像用于大型堆并不是一个好主意。

让我们优化一下代码,看看原生镜像是否比 JIT 运行得更快。这是优化的代码,它执行完全相同的逻辑,但使用更少的堆:

公共类 FibonacciCalculator2{    

         public long findFibonacci(int count) {

                  int fib1 = 0;

                  int fib2 = 1;

                  int currentFib, index;

                  长总=0;

                  for(index=2; index < count; ++index ) {         

                            currentFib = fib1 + fib2;         

                            fib1 = fib2;         

                            fib2 = currentFib;         

                            总计 += currentFib;

                  }

                  总回报;

         }

         public static void main(String args[]) {         

                  斐波那契计算器2 fibCal =      ;               新斐波那契计算器2();

                  长启动时间=       ;               System.currentTimeMillis();

                  现在长=0;

                  持续时间=开始时间;

                  for (int i = 1000000000; i < 1000000010; i++)                   {

                            fibCal.findFibonacci(i);

                            现在 = System.currentTimeMillis();

                            System.out.printf("%d (%d                                 ms)%n", i , now - last);

                            最后=现在;

                  }

                  长结束时间=       ;                System.currentTimeMillis();

                  System.out.printf(" 总计: (%d ms) %n",                            System.currentTimeMillis() -                                     开始时间);

         }

}

以下是使用 Graal JIT 和 Native Image 运行此代码的最终结果。正如您将在以下屏幕截图中看到的那样,Native Image 不需要任何时间来启动并且执行速度比 JIT 快得多。让我们使用 Graal JIT 运行优化后的代码。以下是运行优化代码后的输出:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.18 – FibonacciCalculator2 – 使用 Graal 运行优化代码

现在让我们运行优化代码的原生镜像。下一个屏幕截图显示了运行本机映像后的输出:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.19 – FibonacciCalculator2 – 将优化代码作为原生镜像运行

如果您绘制性能图表,您可以看到显着的改进:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.20 – FibonacciCalculator2 – Graal JIT 与 Native Image

了解 AOT 编译的限制并使用正确的方法很重要。让我们快速浏览一些用于构建原生镜像的编译器配置。

Native image configuration

原生镜像构建是高度可配置的,并且始终建议在 native-image.properties 文件中提供所有构建配置。由于native-image工具需要一个JAR文件作为输入,建议将native-image.properties打包到META-INF/native-image/ JAR 文件中的 <unique-application-identifier>。唯一的应用程序标识符用于避免任何资源冲突。这些路径必须是唯一的,因为它们将在 CLASSPATH 上进行配置。 native-image 工具使用 CLASSPATH 在构建时加载这些资源。除了native-image.properties,还有其他各种可以打包的配置文件。我们将在本节中介绍一些重要的配置。

以下是 native-image.properties 文件的典型格式,随后是属性文件中每个部分的说明:

Requires = <空格分隔的所需语言列表>

JavaArgs = <我们要传递给 JVM 的 Javaargs>

Args = <我们想要传递的原生图像参数>

  • Requires: The Requires property is used to list all the language examples, such as language:llvm language:python.
  • JavaArgs: We can pass regular Java arguments using this property.
  • ImageName: This can be used to provide a custom name for the native image that is generated. By default, the native image is named after the JAR file or Mainclass file (all in lowercase letters). For example, our FibonnaciCalculator.class generates fibonaccicalculator.
  • Args: This is the most commonly used property. It can be used to provide the native-image arguments. The arguments can also be passed from the command line, but it is much better from the configuration management perspective to have them listed in native-image.properties, so that it can go into Git (or any source code repository) and track any changes. The following table explains some of the important arguments that are typically used:
读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

请参考 https://www.graalvm.org/reference-manual/native -image/Options/ 用于完整的选项列表。

Hosted options and resource configurations

我们可以使用各种参数配置各种资源。这些 资源声明通常配置在外部 JSON 文件中,并且可以使各种 -H: 标志指向这些资源文件。语法为 -H =${.}/jsonfile.json 。下表列出了一些使用的重要参数:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像 读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

native-images.properties 捕获所有配置参数,最好通过 native-image.properties 文件传递​​配置,因为它很容易在一个源代码配置管理工具。

GraalVM 带有一个 代理,可以在运行时跟踪 Java 程序的动态特性。这有助于识别和配置具有动态功能的 原生图像构建。要使用 Native 映像代理运行 Java 应用程序,我们需要传递 -agentlib:native-image-agent=config-output-dir=<path to config dir>。代理跟踪执行并拦截查找类、方法、资源和代理的调用。然后代理生成 jni-config.json、reflect-config.json、proxy-config.json 和 resource-config config 目录中的 .json 作为参数传递。使用不同的测试用例多次运行应用程序是一个很好的做法,以确保覆盖完整的代码,并且代理可以捕获大部分动态调用。当我们运行迭代时,重要的是使用 -agentlib:native-image-agent=config-merge-dir= 以便配置文件不会被覆盖而是合并。

我们可以用原生镜像生成 Graal 图表,来分析原生镜像是如何运行的。在下一节中,我们将探讨如何生成这些 Graal 图。

Generating Graal graphs for native images

甚至可以为原生图像生成 Graal 图。 Graal 图可以在构建时或运行时生成。让我们使用我们的 FibonnaciCalculator 应用程序在本节中探索此功能。

让我们使用以下命令为 FibonacciCalculator 生成转储:

native-image -H:Dump=1 斐波那契计算器

以下是输出:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像 native-image -H:Dump=1 FibonacciCalculator

[fibonaccicalculator:54143]  类列表:  811.75 ms,    0.96 GB

[fibonaccicalculator:54143]      (上限):  4,939.64 ms,  0.96 GB

[fibonaccicalculator:54143]      设置:  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]  (对象):  3,235.92 ms,  2.29 GB

[fibonaccicalculator:54143] (特征):  169.55 ms,    2.29 GB

[fibonaccicalculator:54143]   分析:  7,550.64 ms,  2.29 GB

[fibonaccicalculator:54143]   宇宙:  295.75 ms,    2.29 GB

[fibonaccicalculator:54143]    (解析):  829.58 ms,    3.18 GB

[fibonaccicalculator:54143]   (内联):  1,357.72 毫秒,  3.18 GB

[使用-Dgraal.LogFile=<路径>;将 Graal 日志输出重定向到文件。]

在 /graal_dumps/2021.02.28.20.32.24.880 中转储 IGV 图

在 /graal_dumps/2021.02.28.20.32.24.880 中转储 IGV 图

[fibonaccicalculator:54143]  (编译):  13,244.17 ms, 4.74 GB

[fibonaccicalculator:54143]    编译:  16,249.17 ms, 4.74 GB

[fibonaccicalculator:54143]      图像:  1,816.98 ms,  4.74 GB

[fibonaccicalculator:54143]      写:  430.54 ms,    4.74 GB

[fibonaccicalculator:54143]    [总计]:  34,247.73 ms, 4.74 GB

此命令为每个初始化的类生成 很多图。我们可以使用 -H:MethodFilter 标志来指定我们想要为其生成图形的类和方法 。该命令看起来像这样:

native-image -H:Dump=1 -H:MethodFilter=FibonacciCalculator.main FibonacciCalculator

请参阅第 4 章中的 Graal 中间表示部分,Graal Just-In- Time Compiler,了解如何阅读这些图表并了解优化代码的机会。优化源代码对于原生映像至关重要,因为没有像我们在即时编译器中那样的运行时优化。

Understanding how native images manage memory

原生镜像 与 Substrate VM 捆绑在一起,后者具有管理内存的功能,包括垃圾收集。正如我们在构建原生映像部分中看到的,堆分配是映像创建的一部分,以加快启动速度。这些是在构建时初始化的类。请参阅图 5.3,了解 Native Image 构建器在执行静态区域分析后如何初始化堆区域。在运行时,垃圾收集器管理内存。 Native Image builder 支持两种垃圾回收配置。让我们在下面的小节中了解这两个垃圾回收配置。

The Serial garbage collector

串行垃圾收集器 (GC) 是 内置到本机映像中的默认设置。这在社区版和企业版上都可用。此垃圾收集器针对低内存占用和小堆大小进行了优化。我们可以使用 --gc=serial 标志来显式使用 Serial GC。 Serial GC 是 GC 的简单实现。

Serial GC 将堆划分为两个区域,即年轻和年老。下图展示了 Serial GC 的工作原理:

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.21 – 串行 GC 堆架构

年轻代用于新对象。当年轻代块已满并且所有未使用的对象都被回收时触发。当老年代块变满时,会触发一次完整的收集。年轻的集合运行得更快,而完整的集合在运行时更耗时。可以使用参数 -XX:PercentTimeInIncrementalCollection 调整此行为。

默认情况下,此百分比为 50。可以增加此百分比以减少完整收集的数量,从而提高性能,但会对内存大小产生负面影响。根据内存分析,在测试应用程序时,我们可以优化此参数以获得更好的性能和内存占用。以下是如何在运行时传递此参数的示例:

./fibonaccicalculator -XX:PercentTimeInIncrementalCollection=40

此参数也可以在构建时传递:

native-image --gc=serial -R:PercentTimeInIncrementalCollection=70 FibonacciCalculator

还有其他参数也可用于微调,例如 -XX:MaximumYoungGenerationSizePercent。此参数可用于调整年轻代块应占整个堆的最大百分比。

Serial GC 是单线程的,适用于小堆。下图显示了串行 GC 的工作原理。应用程序线程 被暂停以回收内存。它被称为Stop the World事件。在此期间,垃圾收集器线程运行并回收内存。如果堆大小很大并且有很多线程在运行,这会对应用程序的性能产生影响。 Serial GC 非常适合堆大小较小的小型进程。

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.22 – 串行 GC 堆流

默认情况下,串行 GC 在启动 GC 线程之前假定堆大小为 80%。这可以使用 -XX:MaximumHeapSizePercent 标志更改。还有 其他标志可用于微调 Serial GC 的性能。

The G1 Garbage Collector

G1 垃圾收集器是 较新的和高级的垃圾收集器实现。这仅在企业版中可用。可以使用标志 --gc=G1 启用 G1 垃圾收集器 。 G1 提供了吞吐量和延迟的正确平衡。吞吐量是运行代码与 GC 的平均时间。更高的吞吐量意味着我们有更多的 CPU 周期用于代码,而不是 GC 线程。延迟是 Stop The World 事件所花费的时间或暂停代码执行所花费的时间。延迟越少,对我们越好。 G1 的目标是高吞吐量和低延迟。下面是它的工作原理。

G1 将整个堆分成小区域。 G1 运行并发线程以查找所有活动对象,Java 应用程序从不暂停,并跟踪区域之间的所有指针,并尝试收集区域以便程序中的暂停时间更短。 G1 也可能移动活动对象并将它们合并到区域中并尝试使区域为空。

读书笔记《supercharge-your-applications-with-graalvm》第 5 章 Graal Ahead-of-Time 编译器和原生镜像

图 5.23 – G1 GC 堆流

上图显示了 G1 GC 如何通过划分区域来工作。将对象分配到区域中是基于尝试在空区域中分配内存并尝试通过将对象合并到区域中来清空区域,例如分区和取消分区。这个想法是优化管理和收集区域。

G1 垃圾收集器比 Serial GC 占用更大的空间,并且适用于运行时间更长、堆大小更大的情况。有多种参数可用于微调 G1 GC 的性能。下面列出了其中的一些(-H 是在构建映像期间传递的参数,-XX 是在运行映像时传递的):

  • -H:G1HeapRegionSize: This is the size of each region.
  • -XX:MaxRAMPercentage: Percentage of physical memory size that is used as the heap size.
  • -XX:ConcGCThreads: Number of concurrent GC threads. This needs to be optimized for the best performance.
  • -XX:G1HeapWastePercent: The garbage collector stops claiming when it reaches this percentage. This will allow lower latency and higher throughput, however, it is critical to set an optimum value, as if it's too high, then the objects will never get collected, and the application memory footprint will always be high.

选择使用正确的垃圾收集器和配置对于应用程序的性能和内存占用至关重要。

Managing the heap size and generating heap dumps

堆大小可以在运行时使用传递给本机映像的以下运行时参数手动设置。 -Xmx 设置最大堆大小,-Xms 设置最小堆大小,-Xmn 设置年轻代区域的大小,以字节为单位。以下是如何在运行时使用这些参数的示例:

./fibonaccicalculator -Xms2m -Xmx10m -Xmn1m

在构建时,我们可以传递参数来配置堆大小。这是一个关键配置,必须非常小心,因为这会直接影响本机映像的内存占用和性能。以下命令是配置最小堆大小、最大堆大小和最大新堆大小的示例:

本机图像 -R:MinHeapSize=2m -R:MaxHeapSize=10m -R:MaxNewSize=1m 斐波那契计算器

对于调试任何内存泄漏和内存管理问题,堆转储是最重要的。我们通常使用 VisualVM 等工具来进行这种堆转储分析。本机映像不是使用 Java 虚拟机工具接口 (JVMTI) 代理构建的,以便在应用程序运行时执行 堆转储跑步。但是,在构建本机映像时,我们可以使用 -H:+AllowVMInspection 标志进行构建。当我们发送 USR1 信号(sudo kill -USR1 或 -SIGUSR1 或 QUIT/BREAK 时,这将创建一个可以生成堆栈转储的本机映像> 键)和运行时编译信息转储,当我们发送 USR2 信号(sudo kill -USR2 或 -SIGUSR2 - 您可以使用 kill 检查确切的信号-l 命令)。此功能仅在企业版中可用。

我们还可以在需要时通过调用 org.graalvm.nativeimage.VMRuntime#dumpHeap 以编程方式创建堆转储。

Building static native images and native shared libraries

静态原生图像是 静态链接的二进制文件,在运行时不需要任何额外的依赖库。当我们将 微服务应用程序构建为原生镜像时,这些非常有用,因此可以轻松地将它们打包到 Docker 中,而无需担心依赖关系。静态图像最适合构建基于容器的微服务。

在编写本书时,此功能仅适用于 Java 11 上的 Linux AMD64。请参考 https://www.graalvm.org/reference-manual/native-image/StaticImages/ 了解最新更新和构建静态原生镜像的过程。

Native Image 构建器还构建共享库。有时您可能希望将代码创建为 共享库,供其他一些 应用程序使用。为此,您必须传递 –shared 标志来构建共享库,而不是可执行库。

Debugging native images

调试原生镜像需要使用调试信息构建镜像。我们可以使用-H:GenerateDebugInfo=1。以下是对 FibonnacciCalculator 使用此参数的示例:

native-image -H:GenerateDebugInfo=1 斐波那契计算器

生成的图像具有 GNU 调试器 (GDB) 形式的调试信息。这可用于在运行时调试代码。以下显示了运行上述命令的输出:

native-image -H:GenerateDebugInfo=1 斐波那契计算器

[fibonaccicalculator:57833]  类列表:  817.01 ms,    0.96 GB

[fibonaccicalculator:57833]      (上限):  6,301.03 ms,  0.96 GB

[fibonaccicalculator:57833]      设置:  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]  (对象):  3,164.39 ms,  1.22 GB

[fibonaccicalculator:57833] (特征):  181.00 ms,    1.22 GB

[fibonaccicalculator:57833]   分析:  7,282.44 ms,  1.22 GB

[fibonaccicalculator:57833]   宇宙:  304.43 ms,    1.22 GB

[fibonaccicalculator:57833]    (解析):  624.60 ms,    1.22 GB

[fibonaccicalculator:57833]   (内联):  989.65 ms,    1.67 GB

[fibonaccicalculator:57833]  (编译):  8,486.97 ms,  3.15 GB

[fibonaccicalculator:57833]    编译:  10,625.01 ms, 3.15 GB

[fibonaccicalculator:57833]      图像:  869.81 ms,    3.15 GB

[fibonaccicalculator:57833]  调试信息:  1,078.95 毫秒,  3.15 GB

[fibonaccicalculator:57833]      写:  2,224.22 ms,  3.15 GB

[fibonaccicalculator:57833]    [总计]:  33,325.95 ms, 3.15 GB

这将生成一个 sources 目录,其中 保存由本机图像生成器生成的缓存。这个缓存带来了 JDSK、GraalVM 和应用程序类来帮助调试。以下是列出 sources 目录内容的输出:

$ ls -la                        ; 

共 8 个

drwxr-xr-x     9 vijaykumarab    员工     288 17 Apr 09:01 .

drwxr-xr-x    10 vijaykumarab    员工     320 4月17日09:01 ..

-rw-r--r--     1 vijaykumarab    员工     1240 4 月 17 日 09:01 FibonacciCalculator.java

drwxr-xr-x     3 vijaykumarab    员工       96 17 Apr 09:01 com

drwxr-xr-x     5 vijaykumarab    员工     160 17 Apr 09:01 java.base

drwxr-xr-x     3 vijaykumarab    员工       96 17 Apr 09:01 jdk.internal.vm.compiler

drwxr-xr-x     3 vijaykumarab    员工       96 17 Apr 09:01 jdk.localedata

drwxr-xr-x     3 vijaykumarab    工作人员       96 17 Apr 09:01 jdk.unsupported

drwxr-xr-x     3 vijaykumarab    员工       96 17 Apr 09:01 org.graalvm.sdk

要调试原生镜像,我们需要 gdb 实用程序。具体方法请参考https://www.gnu.org/software/gdb/为您的目标机器安装 gdb。一旦正确安装,我们应该可以通过执行gdb命令进入gdb shell。下面显示了典型的输出:

$gdb                   

GNU gdb (GDB) 10.1

版权所有 (C) 2020 Free Software Foundation, Inc.

许可证 GPLv3+:GNU GPL 版本 3 或更高版本 <http://gnu.org/licenses/gpl.html>

这是免费软件:您可以自由更改和重新分发它。

在法律允许的范围内,不提供任何保证。

键入“显示复制”和“显示保修”以了解详细信息。

此 GDB 配置为“x86_64-apple-darwin20.2.0”。

键入“显示配置”以获取配置详细信息。

有关错误报告的说明,请参阅:<https://www.gnu.org/software/gdb/bugs/>。

在线查找 GDB 手册和其他文档资源:

         <http://www.gnu.org/software/gdb/documentation/>.

如需帮助,请键入“帮助”。

键入“apropos word”以搜索与“word”相关的命令。(gdb)

我们需要指向我们在上一步中生成源文件的 目录。我们可以通过执行以下命令来做到这一点:

设置目录 / /jdk:/ /graal:/ /src

设置好环境后,我们可以使用 gdb 设置断点并进行调试。请参考https://www.gnu.org/software/gdb/documentation/ 有关如何使用 gdb 调试可执行文件的详细文档。

在编写本书时,调试信息可用于执行断点、单步执行、堆栈回溯、打印原始值、对象的转换和打印、路径表达式以及方法名称和静态数据的引用。请参考 https://www.graalvm.org/reference-manual/native -image/DebugInfo/ 了解更多详情。

Limitations of Graal AOT (Native Image)

在本节中,我们将介绍 Graal AOT 和原生图像的一些限制。

Graal 提前编译使用封闭世界假设执行静态分析。它假定在​​运行时可访问的所有类在构建时都可用。这对编写任何需要动态加载的代码有直接的影响——例如反射、JNI、代理等。但是,Graal AOT 编译器 (native-image) 提供了一种 方法以 JSON 清单文件的形式提供此元数据。这些文件可以与 JAR 文件一起打包,作为编译器的输入:

  • Loading classes dynamically: Classes that are loaded at runtime, which will not be visible to the AOT compiler at build time, need to be specific in the configuration file. These configuration files are typically saved under META-INF/native-image/, and should be in CLASSPATH. If the class is not found during the compilation of the configuration file, it will throw a ClassNotFoundException.
  • Reflection: Any call to the java.lang.reflect API to list the methods and fields or invoke them using the reflection API has to be configured in the reflect-config.json file under META-INF/native-image/. The compiler tries to identify these reflective elements through static analysis.
  • Dynamic Proxy: Dynamic proxy classes that are generated instances of java.lang.reflect.Proxy need to be defined during build time. The interfaces need to be configured in proxy-config.json.
  • Java Native Interface (JNI): Like reflection, JNI also accesses a lot of class information dynamically. Even these calls need to be configured in jni-config.json.
  • Serialization: Java serialization also accesses a lot of class metadata dynamically. Even these accesses need to be configured ahead of time.

您可以在此处找到有关其他限制的更多详细信息:https://www.graalvm .org/reference-manual/native-image/Limitations/

GraalVM containers

GraalVM 也打包为 Docker 容器。它可以直接从 Docker Registry (ghcr.io) 中提取,也可以用作构建自定义镜像的基础镜像。以下是使用 GraalVM 容器的一些关键命令:

  • To pull the Docker image: docker pull ghcr.io/graalvm/graalvm-ce:latest
  • To run the container: docker run -it ghcr.io/graalvm/graalvm-ce:latest bash
  • To use in the Dockerfile as a base image: FROM ghcr.io/graalvm/graalvm-ce:latest

我们将在 中探索 更多关于 GraalVM 容器的信息第 9 章GraalVM Polyglot – LLVM、Ruby 和 WASM,当我们谈论在 GraalVM 上构建微服务时。

Summary

在本章中,我们详细介绍了 Graal 即时编译器和提前编译器。我们获取了示例代码并查看了 Graal JIT 如何执行各种优化。我们还详细介绍了如何理解 Graal 图。这是关键知识,有助于分析和识别我们可以在开发过程中进行的优化,以加快运行时的 Graal JIT 编译。

本章提供了有关如何构建原生镜像以及如何使用配置文件引导优化来优化原生镜像的详细说明。我们获取了示例代码并编译了本机映像,并且还了解了本机映像的内部工作原理。我们发现了可能导致本机映像运行速度比即时编译器慢的代码问题。我们还介绍了本机图像的限制,以及何时使用本机图像。我们探索了各种构建时间和运行时配置,以优化构建和运行原生镜像。

在下一章中,我们将深入了解 Truffle 语言实现框架以及如何构建多语言应用程序。

Questions

  1. How are native images created?
  2. What is points-to analysis?
  3. What is region analysis?
  4. What are the Serial GC and the G1 GC?
  5. How do you optimize native images? What is PGO?
  6. What are the limitations of native images?

Further reading