Quarkus Native 采用 Adaptive GC 策略


从 Quarkus 2.13.6.Final 开始,本机Native运行时垃圾收集策略已切换,以提供更一致和可预测的运行时性能。

在 2022 年的某个时候,在进行一些本机运行时性能基准测试时,我们观察到,在恒定负载纯文本基准测试中,内存消耗会持续增长,直到达到 500MB 左右,然后才会下降:
小型垃圾收集经常发生,但这些收集无法完全收集所有垃圾。
这种未收集的垃圾将继续增长,直到达到 500MB 左右,此时将进行完整的垃圾收集,并清除不断增长的泄漏。

我们想知道的第一件事是,这个 ~500MB 的限制是什么以及它来自哪里。为此,我们启用了 GC 日志记录以查看是否可以获得一些线索:

$ quarkus-project/target/quarkus-project-1.0.0-SNAPSHOT-runner -XX:+PrintGC -XX:+VerboseGC
2023-01-09 13:29:32,155 INFO  [io.quarkus] (main) quarkus-project 1.0.0-SNAPSHOT native (powered by Quarkus 2.15.2.Final) started in 0.017s. Listening on: http://0.0.0.0:8080
...
[Heap policy parameters:
  YoungGenerationSize: 268435456
      MaximumHeapSize: 27487790640
      MinimumHeapSize: 536870912 <--
     AlignedChunkSize: 1048576
  LargeArrayThreshold: 131072]

我们意识到这个数字实际上是 512MB,这是 GraalVM 在最大堆大小超过 ~3GB 物理内存时配置的默认最小堆大小。

下一个问题是,为什么最小堆大小和出现完整 GC 时的内存消耗之间存在关系?
查看上面的输出,在我们的系统上默认的最大堆大小是 25.6GB。如果没有通过特定配置,GraalVM 默认最大堆大小为物理内存的 80%,实际上 25.6GB 是 32GB 的 80%。在消耗了 512MB 时执行完整 GC 似乎很奇怪,因为我们的系统给它的最大堆大小要大得多。

答案在 Quarkus 明确配置的 GC 策略中找到。

默认情况下,GraalVM 使用称为“自适应”的 GC 策略,但 Quarkus 指示 GraalVM 使用另一种称为“按空间和时间”的 GC 策略。可以在 此处找到有关 Quarkus 为什么使用不同 GC 策略的完整故事,但总而言之,该决定是在 2018 年做出的,当时“按空间和时间”似乎产生了更少的完整 GC,并提供了更好的吞吐量。

“按空间和时间”GC 策略实现了一种shouldCollectCompletely方法,该方法决定是进行完整(完全)收集还是增量(最小)收集。“按空间和时间”GC策略的相关代码如下:

return estimateUsedHeapAtNextIncrementalCollection().aboveThan(getMaximumHeapSize()) // (1)
  || GCImpl.getChunkBytes().aboveThan(getMinimumHeapSize()) && enoughTimeSpentOnIncrementalGCs();
// (2)


执行完整 GC 的一种选择:

  • (1)一个选择是当它估计已用堆将超过最大堆大小时,但我们的情况并非如此。
  • (2)另一个选择是如果发生了足够的最小收集并且使用的堆超过最小堆大小。

后一种选择就是这里发生的事情。

此时我们想,关于默认 GC 策略所做的假设在 2022 年是否仍然适用?因此,我们删除了 GC 策略配置调整。
对于相同的工作负载,默认的 GC 策略(称为“自适应”)与“按空间和时间”策略相比,消耗的堆减少了近 50%。

对于相同的工作负载,“自适应”策略获得的吞吐量与“按空间和时间”策略获得的吞吐量相差不到 1%。因此,“自适应”与“按空间和时间”获得几乎相同的吞吐量,并且内存消耗减少近 50%,这使得切换到“自适应”GC 策略作为 Quarkus 的默认策略成为一个非常有说服力的论据,因为其他 GraalVM 已经是这种情况。

我们经常看到测试或基准测试在没有-Xmx配置的情况下运行,在这种情况下,如上所述,最大堆大小设置为可用物理内存的 80%,并且这个堆大小在现代硬件上很容易超过 3GB。这些用户将通过“自适应”GC 策略看到更好的开箱即用体验。

因此,从 Quarkus 2.13.6.Final 开始,Quarkus 原生应用程序的 GC 策略与 GraalVM 的默认值保持一致,称为“自适应”。