第5章:深入元空间

在第4章中,我们详细研究了垃圾收集。我们发现,没有引用的对象符合垃圾收集的条件。实际上,垃圾收集器标记那些与栈有连接的对象,将其标注为存活对象。然后,垃圾收集器的清除阶段回收未被标记(死对象)的对象的内存。

我们还研究了各种垃圾收集的实现。根据你的具体标准,需要对每种实现进行评估。

本章聚焦于一个称为元空间(Metaspace)的区域。我们将按以下主题研究元空间:

  • JVM 对元空间的使用
  • 类加载
  • 释放元空间内存

让我们从 JVM 对元空间的使用开始。

JVM 对元空间的使用

元空间是堆外本地内存的一个特殊区域。本地内存是由操作系统提供给应用程序供其自身使用的内存。JVM 使用元空间来存储类相关信息,即类的运行时表示。这是类的元数据(metadata);因此元数据存储在元空间中。

元数据

元数据是关于数据的信息。例如,数据库中的列是关于列中数据的元数据。因此,如果一个列名为 Name,而某个具体行的值为 John,那么 Name 就是关于 John 的元数据。

这些元数据包括以下内容:

  • 类文件
  • 类的结构和方法
  • 常量
  • 注解
  • 优化

因此,在元数据中,JVM 拥有处理该类所需的一切信息。

PermGen(永久代)

在 Java 8 之前,元数据存储在与堆连续的一个区域中,称为 PermGen(永久代)。PermGen 存储了类元数据、字符串常量池(interned strings)以及类的静态变量。从 Java 8 开始,类元数据现在存储在元空间中,而字符串常量池和类的静态变量存储在堆上。

现在让我们来研究类加载。

类加载

当某个类首次被访问时(例如,当创建该类的一个对象时),类加载器会定位类文件并在元空间中分配其元数据。类加载器拥有这些分配的元空间,而类加载器实例本身则加载到堆上。一旦加载完成,后续的引用会重用同一类元数据。

此时有两个值得提及的类加载器:引导类加载器(bootstrap class loader,负责加载类加载器本身)和应用程序类加载器(application class loader)。这两个类加载器的元数据永久驻留在元空间中,因此永远不会被垃圾收集。

另一方面,动态类加载器(dynamic class loaders)及其所加载的类符合垃圾收集的条件。

这就引出了元空间内存的释放。

释放元空间内存

从 PermGen(Java 8 之前)到元空间(Java 8 及以后)的一个主要变化是:元空间现在可以动态增长。默认情况下,元空间分配的内存量是无上限的,因为它是本地内存的一部分。可以通过 JVM 标志 -XX:MetaspaceSize 自定义元空间的大小。

元空间仅在两种情况下触发垃圾收集:

  1. 元空间耗尽内存
  2. 元空间大小超过 JVM 设定的阈值

让我们分别研究这两种情况。

元空间耗尽内存

如上所述,默认情况下,元空间可用的本地内存是无限的。如果你用尽内存,会收到 OutOfMemoryError 消息,并且这会触发一次垃圾收集。你可以使用 JVM 标志 -XX:MaxMetaspaceSize 限制元空间的大小。如果达到此限制,也会触发一次垃圾收集。

元空间大小超过 JVM 设定的阈值

我们可以配置 JVM,在元空间达到某个阈值(称为高水位线)时触发垃圾收集。此外,我们可以根据垃圾收集的结果动态调整此阈值。提高高水位线可以防止过快再次触发垃圾收集。降低高水位线则相反:有助于更快地触发下一次垃圾收集。该阈值(高水位线)初始设置为 JVM 标志 -XX:MetaspaceSize 的值。我们使用 -XX:MinMetaspaceFreeRatio-XX:MaxMetaspaceFreeRatio 标志来分别提高或降低高水位线。

现在我们知道垃圾收集在元空间中何时运行,让我们研究垃圾收集在元空间方面是如何工作的。

元空间的垃圾收集

由于类加载器拥有类的元数据,垃圾收集器只能在类加载器本身死亡时才能回收这些元数据。类加载器只有在它所加载的所有类都没有任何实例时才算死亡。

让我们看一个例子来帮助进一步解释这一点。该示例假设使用一个动态类加载器,并使用简化的图示以便于说明。

图 5.1 描述了创建了两个 O 类型对象和一个 P 类型对象之后内存中的情况。

图 5.1 – 元空间分配

在上图中,初始时,JVM 在堆上创建了类加载器对象(深蓝色)、两个 O 类型对象(浅蓝色)和一个 P 类型对象(黄色)。OP 的引用位于栈上。在创建第一个 OP 实例时,类加载器将 OP 的元数据加载到元空间中。但是,在创建第二个 O 实例时,元空间中不会发生任何操作,因为 O 的元数据已经加载完毕。

图 5.2 将展示当两个 O 引用都超出作用域但垃圾收集尚未运行时内存中的情况:

图 5.2 – 元空间(两个 O 引用超出作用域)

如您所见,JVM 已将两个 O 引用从栈中弹出。垃圾收集尚未运行,因此实例仍保留在堆上。图 5.3 显示了第一次垃圾收集运行后的情况:

图 5.3 – 垃圾收集后的元空间(第 1 次运行)

在上图中,我们可以看到垃圾收集器从堆中回收了两个(死亡)O 对象。此外,垃圾收集器将类加载器和 P 对象都移到了幸存区(survivor space)。

请注意,即使堆上没有 O 类型的对象,O 的元数据仍然保留在元空间中。这是因为垃圾收集器无法回收 O 的类加载器,因为堆上还存在 P 类型的对象(同一个类加载器同时加载了 OP)。

图 5.4 显示了当 P 引用超出作用域且垃圾收集再次运行时内存中的情况:

图 5.4 – 垃圾收集后的元空间(第 2 次运行)

我们可以看到 JVM 已将 P 的引用从栈中弹出。结果,垃圾收集器回收了 P 类型的对象。

由于垃圾收集器现在已回收了 OP 类型的所有实例,因此它可以回收加载了 OP 的类加载器。最后,垃圾收集器终于可以回收元空间中 OP 类的元数据。

本章到此结束。让我们回顾一下主要要点。

总结

在本章中,我们深入研究了元空间(以前称为 PermGen)。元空间是堆外内存的一个特殊区域,专门用于存储类的元数据。元数据包含使 JVM 能够处理类的信息,例如方法字节码、常量和注解。当类首次被使用时,其元数据被加载到元空间中。例如首次创建对象时。

默认情况下,元空间可用的本地内存是无限的。可以使用 JVM 标志 -XX:MaxMetaspaceSize 配置最大元空间大小。可以使用 -XX:MetaspaceSize 标志初始设置一个阈值(高水位线)。如果设置了阈值并达到该阈值,则会触发一次垃圾收集。通过结合使用 -XX:MinMetaspaceFreeRatio-XX:MaxMetaspaceFreeRatio 这两个 JVM 标志以及垃圾收集的结果,我们可以动态影响高水位线,从而影响下一次垃圾收集的间隔。

我们通过一个例子看到,类的元数据会一直保留在元空间中,直到垃圾收集器释放加载该类的类加载器。而这只有在那个类加载器所加载的所有类都没有实例时才能发生。

既然我们已经深入研究了元空间,接下来我们将关注下一章,该章重点介绍 JVM 内存管理的配置和监控。


图片说明

原书包含图 5.1、5.2、5.3、5.4,分别位于第 118-120 页。由于无法获取图片内容,上述文字已根据上下文描述了每幅图的关键信息。如果你有原始图片,可以将其插入到对应位置。