跳转到内容

JEP 387:弹性元空间

原文:https://openjdk.org/jeps/387
翻译:张欢

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

  • 改变压缩类指针的编码方式,或改变压缩类空间存在的事实并不是目标。
  • 将元空间分配器的使用扩展到HotSpot的其他区域并不是目标,尽管这可能是未来的增强功能。

JEP 122中引入以来,元空间就因过高的堆外内存使用率而臭名昭著。大多数普通应用程序不会遇到问题,但很容易以错误的方式触发元空间分配器,从而导致过多的内存浪费。不幸的是,这类病态情况并不少见。

元空间内存在每个类加载器对应的arenas中进行管理。一个arenas包含一个或多个chunk,加载器通过低成本的指针碰撞从这些chunk中分配内存。元空间chunk的粒度较粗,以保持分配操作的高效。然而,这可能会导致使用大量小型类加载器的应用程序遭受不合理的高元空间使用率。

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

我们建议用基于伙伴的分配模式取代现有的元空间内存分配器。这是一种古老且成熟的算法,已在Linux内核等系统中成功应用。该方案使得以较小的块分配元空间内存变得可行,从而减少类加载器的开销。它还可以减少碎片,从而允许我们将未使用的元空间内存返回给操作系统,以提高弹性。

我们还将按需将操作系统的内存延迟提交给arenas。这将减少加载器(例如启动类加载器)占用的内存,这些加载器最初拥有较大的arenas,但不会立即使用它们,或者可能永远不会充分利用它们。

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

详细描述新算法的文档可在这里找到。工作原型存在于JDK沙盒仓库的分支中。

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

  • 作为一个基于arena的分配器,元空间利用了类元数据对象批量释放的特性。C堆分配器没有这种优势,所以我们必须单独跟踪和释放每个对象。这会增加运行时开销,并且根据对象的跟踪方式,代码复杂度和/或内存使用量也会增加。
  • 元空间使用指针碰撞分配,从而实现非常紧密的内存打包。C堆分配器通常每次分配都会产生更多开销。
  • 如果我们使用C堆分配器,那么我们就无法像今天这样实现压缩类空间,并且必须为压缩类指针提出不同的解决方案。
  • 过度依赖C配器本身也存在风险。C堆分配器本身也存在一些问题,例如碎片化严重、弹性较差。由于这些问题不受我们控制,因此解决这些问题需要与操作系统供应商合作,这可能会耗费大量时间,并且很容易抵消降低代码复杂度带来的优势。

尽管如此,我们还是测试了一个将元数据分配重新连接到C堆的原型。我们将这个基于malloc的原型与上面描述的基于伙伴的原型进行了比较,并运行了一个涉及大量类加载和卸载的微基准测试。由于压缩类空间无法与C堆分配兼容,因此我们在此测试中关闭了它。

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

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

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

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

实践中,伙伴分配器的碎片整理能力相当出色,因此我们观察到内存映射数量略有增加。如果映射数量的增加会造成问题,那么我们会加大粒度,从而降低取消提交的粗粒度。这会减少虚拟内存映射的数量,但会损失一些取消提交的机会。

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

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

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

-XX:MetaspaceReclaimPolicy=(balanced|aggressive|none)

  • balanced:大多数应用程序应该会看到元空间内存占用的改善,而内存回收的负面影响应该很小。这是默认模式,旨在实现向后兼容。
  • aggressive:以增加虚拟内存碎片为代价,提供更高的内存回收率。
  • none:完全禁用内存回收。

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