Quarkus 原生应用程序发生 OutOfMemoryError 时获取堆转储

从 JDK 21 的 GraalVM 开始,原生可执行文件可以使用 -XX:+HeapDumpOnOutOfMemoryError 选项在抛出 java.lang.OutOfMemoryError 时生成堆转储。在本篇博客文章中,我们将探讨如何使用此标志,检查 GraalVM 原生镜像的堆转储是什么样的,以及它与 HotSpot 生成的堆转储有何不同。

注意:GraalVM 原生镜像可执行文件的堆同时包含只读和读写段。只读部分称为“镜像堆”,其中包含在镜像构建期间预先初始化的数据,以帮助加快启动速度。读写部分是在运行时进行分配的地方。因此,在运行时生成的堆转储将包含“镜像堆”和读写堆的内容。

要让此标志生效,我们需要制造一个 Quarkus 应用程序耗尽内存的情况。一种简单的方法是配置应用程序使用一个不进行任何内存回收的垃圾收集器,即 Epsilon GC。一旦 Quarkus 应用程序以 Epsilon GC 运行,施加一些负载,在短时间内它就会耗尽内存并生成堆转储。

让我们以一个简单的响应 HTTP 端点请求的 Quarkus 应用程序作为起点来实现这一点。可以使用浏览器或命令行通过 code.quarkus.io 下载示例应用程序。

$ wget "https://code.quarkus.io/d?S=io.quarkus.platform%3A3.8&cn=code.quarkus.io" -O code.zip
$ unzip code.zip
$ cd code-with-quarkus

接下来,使用 GraalVM for JDK 21 构建一个 Quarkus 原生可执行文件,并配置它使用 Epsilon GC。

$ ./mvnw package -DskipTests -Dnative -Dquarkus.native.additional-build-args=--gc=epsilon

注意:对于 Quarkus 原生应用程序,GC 选择需要在构建时完成。

在生成 Quarkus 原生可执行文件的过程中,您将能够观察到 GC 确实被配置为 EpsilonGC。

[INFO] --- quarkus-maven-plugin:3.9.3:build (default) @ getting-started-reactive ---
...
[1/8] Initializing...
 Java version: 21.0.2+13, vendor version: GraalVM CE 21.0.2+13.1
 Graal compiler: optimization level: 2, target machine: compatibility
 C compiler: gcc (redhat, x86_64, 13.2.1)
 Garbage collector: Epsilon GC (max heap size: 80% of RAM)

构建完成后,使用 -XX:+HeapDumpOnOutOfMemoryError -XX:+ExitOnOutOfMemoryError 启动 Quarkus。后者会在发生 OutOfMemoryError 时强制应用程序关闭,而不是让进程处于不确定的状态。

$ getting-started-reactive-1.0.0-SNAPSHOT-runner -XX:+HeapDumpOnOutOfMemoryError -XX:+ExitOnOutOfMemoryError -Xmx64m
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2024-04-12 09:58:00,855 INFO  [io.quarkus] (main) getting-started-reactive 1.0.0-SNAPSHOT native (powered by Quarkus 3.9.3) started in 0.030s. Listening on: http://0.0.0.0:8080
2024-04-12 09:58:00,855 INFO  [io.quarkus] (main) Profile prod activated.
2024-04-12 09:58:00,855 INFO  [io.quarkus] (main) Installed features: [cdi, rest, smallrye-context-propagation, vertx]

接下来,对 Quarkus 应用程序暴露的 HTTP 端点施加一些负载,例如使用 bash 循环中的 curl

$ while true; do curl https://:8080/hello; done

切换到 Quarkus 控制台,您将看到 OutOfMemoryError 以及一些指示正在生成堆转储的消息。

2024-04-12 09:58:00,855 INFO  [io.quarkus] (main) getting-started-reactive 1.0.0-SNAPSHOT native (powered by Quarkus 3.9.3) started in 0.030s. Listening on: http://0.0.0.0:8080
2024-04-12 09:58:00,855 INFO  [io.quarkus] (main) Profile prod activated.
2024-04-12 09:58:00,855 INFO  [io.quarkus] (main) Installed features: [cdi, rest, smallrye-context-propagation, vertx]
Dumping heap to svm-heapdump-2213-OOME.hprof ...
Heap dump file created [98454651 bytes in 0.733 secs]
Terminating due to java.lang.OutOfMemoryError: Garbage-collected heap size exceeded.

市面上有多种图形化工具可以检查堆转储,例如 VisualVMEclipse Memory Analyzer (MAT) 等,但也有一些新的工具可以在命令行运行,以快速了解堆转储的概况,例如 Java Object Layout (JOL)

$ java -Xmx256m -DprintFirst=6 -jar jol-cli.jar heapdump-stats svm-heapdump-2213-OOME.hprof
Heap Dump: svm-heapdump-2213-OOME.hprof
Read progress: DONE

Hotspot Layout Simulation (JDK 21, Current VM: 12-byte object headers, 4-byte references, 8-byte aligned objects, 8-byte aligned array bases)

=== Class Histogram

Table is sorted by "INSTANCES".
Printing first 6 lines. Use -DprintFirst=# to override.

       INSTANCES            SIZE        SUM SIZE    CLASS
------------------------------------------------------------------------------------------------
         132,330              24       3,175,920    java.lang.String
          50,277              40       2,011,080    io.vertx.core.http.impl.headers.HeadersMultiMap$MapEntry
          44,852              40       1,794,080    com.oracle.svm.core.monitor.JavaMonitor
          38,755              32       1,240,160    java.util.HashMap$Node
          33,776              24         810,624    byte[5]
          31,728              48       1,522,944    java.util.HashMap
             ...             ...             ...    ...
         899,336       9,438,056      48,193,360    <other>
------------------------------------------------------------------------------------------------
       1,231,054       9,438,264      58,748,168    <total>

=== Class Histogram

Table is sorted by "SIZE".
Printing first 6 lines. Use -DprintFirst=# to override.

       INSTANCES            SIZE        SUM SIZE    CLASS
------------------------------------------------------------------------------------------------
               1       3,480,544       3,480,544    byte[3480528]
               1       3,236,728       3,236,728    byte[3236705]
               1         642,648         642,648    byte[642626]
               1         289,824         289,824    byte[289808]
               1         173,664         173,664    byte[173645]
               1         157,728         157,728    byte[157710]
             ...             ...             ...    ...
       1,231,048       1,457,128      50,767,032    <other>
------------------------------------------------------------------------------------------------
       1,231,054       9,438,264      58,748,168    <total>

=== Class Histogram

Table is sorted by "SUM SIZE".
Printing first 6 lines. Use -DprintFirst=# to override.

       INSTANCES            SIZE        SUM SIZE    CLASS
------------------------------------------------------------------------------------------------
               1       3,480,544       3,480,544    byte[3480528]
               1       3,236,728       3,236,728    byte[3236705]
         132,330              24       3,175,920    java.lang.String
          50,277              40       2,011,080    io.vertx.core.http.impl.headers.HeadersMultiMap$MapEntry
          10,054             184       1,849,936    io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext
          44,852              40       1,794,080    com.oracle.svm.core.monitor.JavaMonitor
             ...             ...             ...    ...
         993,539       2,720,704      43,199,880    <other>
------------------------------------------------------------------------------------------------
       1,231,054       9,438,264      58,748,168    <total>

通过堆转储中存在的 com.oracle.svm.core.monitor.JavaMonitor 实例,可以清晰地观察到驱动 GraalVM 原生镜像的可执行文件 VM SubstrateVM。如果我们重复完全相同的练习,但改用 Quarkus JVM 模式,堆转储会是什么样子?让我们来看看。

为 JVM 模式重新构建 Quarkus 应用,并使用 Epsilon GC 运行它。

$ mvnw package -DskipTests
$ java -XX:+HeapDumpOnOutOfMemoryError -XX:+ExitOnOutOfMemoryError -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -Xmx64m -jar quarkus-run.jar
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2024-04-12 10:02:37,945 INFO  [io.quarkus] (main) getting-started-reactive 1.0.0-SNAPSHOT on JVM (powered by Quarkus 3.9.3) started in 1.692s. Listening on: http://0.0.0.0:8080
2024-04-12 10:02:37,961 INFO  [io.quarkus] (main) Profile prod activated.
2024-04-12 10:02:37,962 INFO  [io.quarkus] (main) Installed features: [cdi, rest, smallrye-context-propagation, vertx]

施加相同的负载后,您也会在 JVM 模式下观察到堆转储的生成。

2024-04-12 10:02:37,945 INFO  [io.quarkus] (main) getting-started-reactive 1.0.0-SNAPSHOT on JVM (powered by Quarkus 3.9.3) started in 1.692s. Listening on: http://0.0.0.0:8080
2024-04-12 10:02:37,961 INFO  [io.quarkus] (main) Profile prod activated.
2024-04-12 10:02:37,962 INFO  [io.quarkus] (main) Installed features: [cdi, rest, smallrye-context-propagation, vertx]
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid2841.hprof ...
Heap dump file created [97383542 bytes in 1.232 secs]
Terminating due to java.lang.OutOfMemoryError: Java heap space

而这便是使用 JOL 时的堆转储样子。

java -Xmx256m -DprintFirst=6 -jar jol-cli.jar heapdump-stats java_pid2841.hprof
Heap Dump: java_pid2841.hprof
Read progress: DONE

Hotspot Layout Simulation (JDK 21, Current VM: 12-byte object headers, 4-byte references, 8-byte aligned objects, 8-byte aligned array bases)

=== Class Histogram

Table is sorted by "INSTANCES".
Printing first 6 lines. Use -DprintFirst=# to override.

       INSTANCES            SIZE        SUM SIZE    CLASS
------------------------------------------------------------------------------------------------
          91,335              24       2,192,040    java.lang.String
          35,946              40       1,437,840    io.vertx.core.http.impl.headers.HeadersMultiMap$MapEntry
          34,942              32       1,118,144    java.util.HashMap$Node
          22,998              24         551,952    byte[5]
          22,789              48       1,093,872    java.util.HashMap
          22,381              32         716,192    java.util.concurrent.ConcurrentHashMap$Node
             ...             ...             ...    ...
         716,986      19,427,976      59,367,272    <other>
------------------------------------------------------------------------------------------------
         947,377      19,428,176      66,477,312    <total>

=== Class Histogram

Table is sorted by "SIZE".
Printing first 6 lines. Use -DprintFirst=# to override.

       INSTANCES            SIZE        SUM SIZE    CLASS
------------------------------------------------------------------------------------------------
               1         972,120         972,120    int[243026]
               1         416,136         416,136    int[104030]
               1         282,056         282,056    int[70510]
               1         237,608         237,608    byte[237587]
               1         131,920         131,920    int[32976]
               1         129,672         129,672    int[32414]
             ...             ...             ...    ...
         947,371      17,258,664      64,307,800    <other>
------------------------------------------------------------------------------------------------
         947,377      19,428,176      66,477,312    <total>

=== Class Histogram

Table is sorted by "SUM SIZE".
Printing first 6 lines. Use -DprintFirst=# to override.

       INSTANCES            SIZE        SUM SIZE    CLASS
------------------------------------------------------------------------------------------------
          91,335              24       2,192,040    java.lang.String
           7,189             232       1,667,848    io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext
          35,946              40       1,437,840    io.vertx.core.http.impl.headers.HeadersMultiMap$MapEntry
          15,528              80       1,242,240    java.util.HashMap$Node[16]
          14,380              80       1,150,400    io.vertx.core.http.impl.headers.HeadersMultiMap$MapEntry[16]
          34,942              32       1,118,144    java.util.HashMap$Node
             ...             ...             ...    ...
         748,057      19,427,688      57,668,800    <other>
------------------------------------------------------------------------------------------------
         947,377      19,428,176      66,477,312    <total>

如预期所示,此堆转储中不存在 SubstrateVM 类,堆转储中只剩下 Quarkus、Vert.x 和 OpenJDK 类型。