跳到主要内容

JEP 387:弹性元空间

概括

更及时地将未使用的 HotSpot 类元数据(即_元空间_)内存返回给操作系统,减少元空间占用空间,并简化元空间代码以降低维护成本。

非目标

  • 它的目标不是改变压缩类指针编码的工作方式,也不是改变压缩类空间存在的事实。

  • 将元空间分配器的使用扩展到 HotSpot 的其他区域并不是目标,尽管这可能是未来的增强功能。

动机

自从JEP 122中出现以来,元空间一直因高堆外内存使用而臭名昭著。大多数正常应用程序不会出现问题,但很容易以错误的方式触发元空间分配器,从而导致过多的内存浪费。不幸的是,这些类型的病理病例并不少见。

元空间内存在每个类加载器区域中进行管理。一个 arena 包含一个或多个_块_,其加载器通过廉价的指针碰撞从中进行分配。元空间块是粗粒度的,以保持分配操作的效率。然而,这可能会导致使用许多小型类加载器的应用程序遭受不合理的高元空间使用率。

当类加载器被回收时,其元空间区域中的块将被放置在空闲列表中以供以后重用。然而,这种重用可能在很长一段时间内都不会发生,或者可能永远不会发生。因此,具有大量类加载和卸载活动的应用程序可能会在元空间空闲列表中积累大量未使用的空间。如果该空间没有碎片化,则可以返回给操作系统以用于其他目的,但情况通常并非如此。

描述

我们建议用基于伙伴的分配方案替换现有的元空间内存分配器。这是一种古老且经过验证的算法,已成功用于 Linux 内核等领域。该方案将使以较小的块分配元空间内存变得可行,这将减少类加载器的开销。它还将减少碎片,这将使我们能够通过将未使用的元空间内存返回给操作系统来提高弹性。

我们还将根据需要将内存从操作系统延迟地提交到竞技场。这将减少从大型区域开始但不立即使用它们或可能永远不会充分利用它们的加载器(例如引导类加载器)的占用空间。

最后,为了充分利用伙伴分配提供的弹性,我们将元空间内存安排成大小统一的_颗粒_,这些颗粒可以彼此独立地提交和取消提交。这些颗粒的大小可以通过新的命令行选项来控制,该选项提供了一种控制虚拟内存碎片的简单方法。

可以在此处找到详细描述新算法的文档。工作原型作为JDK 沙箱存储库中的一个分支存在。

备择方案

我们可以删除元空间并直接从 C 堆中分配类元数据,而不是对元空间进行现代化改造。这种更改的优点是降低代码复杂性。然而,使用 C 堆分配器有以下缺点:

  • 作为基于竞技场的分配器,元空间利用了类元数据对象被批量释放的事实。 C 堆分配器没有那么奢侈,因此我们必须单独跟踪和释放每个对象。这会增加运行时开销,并且取决于对象的跟踪方式、代码复杂性和/或内存使用情况。

  • 元空间使用指针碰撞分配,实现了非常紧密的内存打包。 C 堆分配器每次分配通常会产生更多开销。

  • 如果我们使用 C 堆分配器,那么我们就无法像今天一样实现压缩类空间,并且必须为压缩类指针提出不同的解决方案。

  • 过度依赖 C 分配器会带来风险。 C 堆分配器可能会带来一系列问题,例如碎片高和弹性差。由于这些问题不在我们的控制范围内,解决这些问题需要与操作系统供应商合作,这可能会耗费大量时间,并且很容易抵消降低代码复杂性的优势。

尽管如此,我们测试了一个将元数据分配重新分配到 C 堆的原型。我们将这种malloc基于伙伴的原型与上面描述的基于伙伴的原型进行了比较,运行涉及重类加载和卸载的微基准测试。我们在此测试中关闭了压缩类空间,因为它不适用于 C 堆分配。

在具有 glibc 2.23 的 Debian 系统上,我们观察到malloc基于 的原型存在以下问题:

  • 性能降低了 8-12%,具体取决于加载类的数量和大小。
  • 在类卸载之前,类加载峰值的内存使用量(进程 RSS)增加了 15-18% 。
  • 内存使用量根本没有从使用高峰中恢复,即元空间完全没有弹性。这导致内存使用率差异高达 153%

这些观察隐藏了由于关闭压缩类空间而导致的内存损失;考虑到这一点将使malloc基于 的变体的比较更加不利。

风险和假设

虚拟内存碎片

每个操作系统都以某种方式管理其虚拟内存范围;例如,Linux 内核使用红黑树。未提交内存可能会使这些范围变得碎片化并增加其数量。这可能会影响某些内存操作的性能。根据操作系统的不同,它还可能导致 VM 进程遇到内存映射最大数量的系统限制。

在实践中,伙伴分配器的碎片整理功能非常好,因此我们观察到内存映射数量有非常适度的增加。如果映射数量的增加成为问题,那么我们将增加颗粒大小,这将导致更粗糙的取消提交。这将减少虚拟内存映射的数量,但代价是失去一些未提交的机会。

未提交速度

取消提交大范围的内存可能会很慢,具体取决于操作系统如何实现页表以及之前填充该范围的密度。元空间回收可能在垃圾收集暂停期间发生,因此这可能是一个问题。

到目前为止,我们还没有观察到这个问题,但如果未提交时间成为一个问题,那么我们可以将未提交工作卸载到一个单独的线程,以便它可以独立于 GC 暂停来完成。

填海政策

为了处理涉及虚拟内存碎片或未提交速度的潜在问题,我们将添加一个新的生产命令行选项来控制元空间回收行为:

`-XX:MetaspaceReclaimPolicy=(balanced|aggressive|none)`
  • balanced:大多数应用程序应该会看到元空间内存占用有所改善,而内存回收的负面影响应该很小。此模式是默认模式,旨在向后兼容。
  • “激进”:以增加虚拟内存碎片为代价提供更高的内存回收率。
  • 'none':完全禁用内存回收。

元数据的最大大小

单个元空间对象不能大于_根块大小_,这是伙伴分配器管理的最大块大小。根块大小当前设置为 4MB,这比我们想要在元空间中分配的任何内容都要大。