JEP 387: Elastic Metaspace | 弹性元空间
摘要
更迅速地将未使用的 HotSpot 类元数据(即 元空间)内存返回给操作系统,减少元空间占用,并简化元空间代码以降低维护成本。
非目标
不改变压缩类指针编码的工作方式,也不改变压缩类空间的存在事实。
不将元空间分配器的使用扩展到 HotSpot 的其他区域,尽管这可能是未来可能的一个增强。
动机
自 JEP 122 推出以来,元空间因高非堆内存使用而备受诟病。大多数正常应用程序没有问题,但很容易以错误的方式触发元空间分配器,从而导致过多的内存浪费。不幸的是,这种病理情况并不罕见。
元空间内存按类加载器进行管理,采用 arenas 方式进行管理。arenas 包含一个或多个 块,其加载器通过廉价的指针递增进行分配。元空间块是粗粒度的,以保持分配操作的效率。然而,这可能会导致使用许多小型类加载器的应用程序元空间使用过高,显得不合理。
当类加载器被回收时,其元空间 arenas 中的块会被放置在空闲列表中以便后续重用。然而,这种重用可能很长时间都不会发生,或者根本不会发生。因此,具有大量类加载和卸载活动的应用程序可能会在元空间空闲列表中累积大量未使用的空间。如果未出现碎片化,则该空间可以被返回给操作系统以供其他用途,但实际情况往往并非如此。
描述
我们提议用基于伙伴的分配方案(buddy-based allocation scheme)替换现有的元空间内存分配器。这是一种古老且经过验证的算法,已在 Linux 内核等系统中成功使用。该方案将使得元空间内存的实际分配能够以更小的块进行,从而减少类加载器的开销。同时,它还将减少碎片化,从而允许我们通过将未使用的元空间内存返回给操作系统来提高弹性。
我们还将根据需求,惰性地将内存从操作系统提交给 arenas。这将减少那些以大型 arenas 开始但并未立即使用或可能永远不会充分利用它们的加载器的占用空间,例如引导类加载器。
最后,为了充分利用伙伴分配提供的弹性,我们将把元空间内存组织成大小统一的 颗粒,这些颗粒可以独立地提交和取消提交。这些颗粒的大小可以通过一个新的命令行选项来控制,这提供了一种简单的控制虚拟内存碎片化的方法。
详细描述新算法的文档可以在 此处 找到。一个正在工作的原型存在于 JDK 沙箱存储库的分支中。
替代方案
除了对元空间进行现代化改造外,我们还可以选择移除它,并直接从 C 堆中分配类元数据。这种变化的优势在于能够减少代码复杂性。然而,使用 C 堆分配器将带来以下缺点:
作为基于 arenas 的分配器,元空间利用了类元数据对象批量释放的事实。而 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
:大多数应用程序应该会看到元空间内存占用的改善,而内存回收的负面影响应该是微不足道的。此模式是默认模式,旨在实现向后兼容性。- 'aggressive':以增加虚拟内存碎片化为代价,提供更高的内存回收率。
- 'none':完全禁用内存回收。
元数据的最大大小
单个元空间对象不能大于 根块大小,这是伙伴分配器管理的最大块大小。目前,根块大小设置为 4MB,这远大于我们在元空间中想要分配的任何内容。