在第 4 章中,我们深入讨论了垃圾回收(GC)。我们得出结论:没有引用的对象有资格被垃圾回收。实质上,垃圾回收器会标记那些还能从栈追溯到的对象,将其注记为存活对象;随后在**清扫阶段(sweep)**回收未被标记(即“死亡对象”)所占用的内存。
我们也考察了多种垃圾回收实现方式。应当依据你的特定需求,对各实现进行评估与取舍。
本章聚焦一个称为 Metaspace(元空间) 的区域。我们将从以下几个方面来考察 Metaspace:
- JVM 如何使用 Metaspace
- 类加载
- 释放 Metaspace 内存
先从 JVM 对 Metaspace 的使用说起。
JVM 对 Metaspace 的使用
Metaspace 是堆外的一块原生内存区域。所谓原生内存(native memory),是操作系统提供给应用程序自行使用的那部分内存。JVM 使用 Metaspace 来存放与类相关的信息,也就是类在运行时的表示。这些信息即类的元数据(metadata) ,因此元数据被存放在 Metaspace 中。
元数据(METADATA)
元数据是关于数据的“数据”。比如,在数据库中,列名就是关于列中数据的元数据。若列名为 Name、某行的值为 John,则 Name 就是关于 John 的元数据。
这些元数据包括:
- 类文件
- 类的结构与方法
- 常量
- 注解
- 优化信息
也就是说,元数据里包含了 JVM 处理该类所需的一切信息。
PermGen(永久代)
在 Java 8 之前,元数据存放在与堆相邻的一块区域,称为 PermGen(永久代) 。PermGen 不仅存储类元数据,还存放字符串常量池(interned strings)和类的静态变量。自 Java 8 起,类元数据迁入 Metaspace;而字符串常量池与类/静态变量则迁回堆上存储。
接下来看看类加载。
类加载
当某个类第一次被访问(例如首次创建该类的对象)时,类加载器会定位到该类文件,并在 Metaspace 中为其分配元数据。这块 Metaspace 由该类加载器“拥有”;而类加载器自身是加载到堆上的。一旦类被加载,后续对该类的引用都会复用同一份元数据。
此处有两个值得一提的类加载器:引导类加载器(bootstrap class loader) (负责加载类加载器本身)和应用类加载器(application class loader) 。这两者的元数据会永久驻留在 Metaspace 中,因此不会被垃圾回收。相比之下,动态类加载器(以及它们加载的类)则是可以被垃圾回收的。
这也引出了下一个话题:如何从 Metaspace 释放内存。
释放 Metaspace 内存
从 PermGen(Java 8 之前)到 Metaspace(Java 8 及之后)的重大变化之一,是 Metaspace 现在可以按需增长。默认情况下,分配给 Metaspace 的内存量不设上限,因为它属于原生内存(native memory)。你可以使用 JVM 标志 –XX:MetaspaceSize 自定义 Metaspace 的大小。
Metaspace 只有在两种情形下会触发垃圾回收(GC):
- Metaspace 内存耗尽
- Metaspace 大小超过 JVM 设定的阈值
下面分别说明。
Metaspace 内存耗尽
如前所述,默认给 Metaspace 的原生内存是无限的。如果出现内存耗尽,会抛出 OutOfMemoryError,并触发一次 GC。你可以用 –XX:MaxMetaspaceSize 限制 Metaspace 的最大值;一旦达到该上限,同样会触发 GC。
Metaspace 大小超过 JVM 设定阈值
可以将 Metaspace 的阈值(也称 高水位线)配置为在达到时触发 GC。此外,还可以根据 GC 结果动态调整这个阈值:
- 提高高水位线:避免太快再次触发 GC;
- 降低高水位线:有助于更快再次触发 GC。
阈值(高水位线)最初由 –XX:MetaspaceSize 的取值决定。我们使用 –XX:MinMetaspaceFreeRatio 与 –XX:MaxMetaspaceFreeRatio 来分别降低或提高高水位线。
既然知道了 Metaspace 何时触发 GC,接下来看看 Metaspace 的垃圾回收如何工作。
Metaspace 的垃圾回收
由于类加载器拥有某个类的元数据,只有当该类加载器本身死亡时,GC 才能回收这些元数据。而类加载器只有在由其加载的所有类都没有任何实例时,才被视为死亡。
下面通过一个示例(假设存在一个动态类加载器,并用简化的示意图)进一步说明。
图 5.1 – Metaspace 分配
JVM 在堆上创建了:类加载器对象(深蓝)、两个 O 类型对象(浅蓝)和一个 P 类型对象(黄色),其引用 O 与 P 位于栈上。首次创建 O 与 P 实例时,类加载器会在 Metaspace 中加载 O 与 P 的元数据。创建第二个 O 实例时,Metaspace 不会变化(因为 O 的元数据已加载)。
图 5.2 – Metaspace(两个 O 的引用离开作用域,GC 尚未运行)
JVM 已从栈上弹出了两个 O 的引用。由于 GC 尚未运行,这两个实例仍留在堆上。
图 5.3 – Metaspace(第一次 GC 之后)
GC 回收了堆上两个已死亡的 O 对象,并把类加载器对象与 P 对象移动到幸存者区。
注意:尽管堆上已无任何 O 类型对象,Metaspace 中的 O 元数据依然存在。原因是:加载 O 的类加载器尚不能被回收——因为堆上仍存在由同一加载器加载的 P 对象。
图 5.4 – Metaspace(第二次 GC 之后)
当 P 的引用也离开作用域并再次触发 GC 后,JVM 回收了 P 对象。
由于 O 与 P 的所有实例都已被回收,加载它们的类加载器也随之被回收。至此,GC 终于可以回收 Metaspace 中 O 与 P 的类元数据。
小结
本章聚焦 Metaspace(早期称 PermGen)。Metaspace 是非堆内存中的一块专用区域,用于存放类元数据。元数据包含 JVM 处理类所需的信息,例如:方法字节码、常量、注解与优化信息。类首次被使用时,其元数据会被加载进 Metaspace(例如第一次创建该类的对象)。
默认情况下,Metaspace 可使用的原生内存不设上限。可以通过 –XX:MaxMetaspaceSize 配置最大值;通过 –XX:MetaspaceSize 设置初始阈值(高水位线)。当达到阈值即可触发 GC。配合 –XX:MinMetaspaceFreeRatio 与 –XX:MaxMetaspaceFreeRatio,并结合 GC 结果,我们可以动态影响高水位线,从而影响到下一次 GC 的触发间隔。
示例还展示了:类的元数据会一直留在 Metaspace 中,直到 GC 能回收加载该类的类加载器。而这只有在该加载器加载的所有类的实例都不存在时才会发生。
下一章我们将转向 JVM 内存管理的配置与监控。