这种文章将会分享一些关于元空间(Metaspace)和压缩类空间(Compressed class space)的细节,以及怎么如何阅读和解析相关的GC日志信息。
元空间
元空间是一个本地内存区域,用于存储类的元数据。当一个类被JVM加载的时候,它的元数据(即它在JVM中运行时形式)被分配到元空间中。
随着越来越多的类被加载,元空间的占用率也会增加。而且,当类加载器(classloader)以及其所有加载的类在Java堆中变得不可访问时,元空间关联的类元数据也将会有资格进行释放。类的元数据通过垃圾回收周期进行清理。
现在你可能会问,在运行GC以清理不需要的元数据空间之前,元空间占用率可以增长多少有限制吗?答案是肯定的。JVM在内部维护元空间占用的阈值(也称为高水位线),并且在分配到元空间时会检查此阈值。当在该阈值内无法满足特定分配时,将调用‘元数据GC阈值’垃圾回收周期,如以下日志记录所示:
2021-11-30T08:50:44.641+0800:
[Full GC (Metadata GC Threshold)
[PSYoungGen: 1168K->0K(90112K)]
[ParOldGen: 1989K->3037K(2104832K)] 3158K->3037K(2194944K),
[Metaspace: 1142318K->1142318K(1265664K)], 0.0914589 secs]
[Times: user=0.10 sys=0.00, real=0.09 secs]
注:本文章日志是基于Java8生成。
有两个 JVM 选项 - MetaspaceSize和MaxMetaspaceSize可用于控制元空间的初始大小和最大大小。如果这些值未在命令行中明确提供,则可以使用 JVM 选项-XX:+PrintFlagsFinal查看 JVM 设置的值。
uintx MaxMetaspaceSize = 18446744073709547520 {product}
uintx MetaspaceSize = 21807104 {pd product}
如我们所见,如果在命令行上未配置MaxMetaspaceSize几乎是无限的。MetaspaceSize设置 Metaspace 的初始容量和阈值,在该容量时会调用 GC 来清理空间,或者在无法回收足够空间时扩展容量。
元空间使用信息
让我们看一下使用将类加载和卸载到元空间的 Java 程序生成的一些日志。我用于执行程序的 JVM 选项是:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -XX:-UseCompressedClassPointers
暂时忽略选项UseCompressedClassPointers ;我们将在下一节中讨论这一点。
在运行时,Java 程序遇到了几次“Metadata GC Threshold”GC。让我们仔细检查一个这样的日志记录。
2021-10-07T14:13:10.847+0800:
[Full GC (Metadata GC Threshold)
[PSYoungGen: 384K->0K(93696K)]
[ParOldGen: 982K->1360K(1172480K)] 1366K->1360K(1266176K),
[Metaspace: 410659K->410659K(456704K)], 0.0418110 secs]
[Times: user=0.04 sys=0.00, real=0.04 secs]
在这里,我们可以看到在这个特定的 Full GC 之前,Metaspace 占用为 410659K,而在 GC 之后占用没有变化。括号中的数字表示新保留的元空间大小,即 456704K。
由于我们添加了-XX:+PrintHeapAtGC选项,因此还会打印 GC 前后的内存使用信息,并帮助我们了解调用此 GC 的原因以及元空间大小如何随此特定集合而变化。
{Heap before GC invocations=16 (full 1):
PSYoungGen total 93696K, used 384K [0x00000007aab00000, 0x00000007b0e80000, 0x0000000800000000)
eden space 91136K, 0% used [0x00000007aab00000,0x00000007aab00000,0x00000007b0400000)
from space 2560K, 15% used [0x00000007b0680000,0x00000007b06e0000,0x00000007b0900000)
to space 2560K, 0% used [0x00000007b0400000,0x00000007b0400000,0x00000007b0680000)
ParOldGen total 919552K, used 982K [0x0000000700000000, 0x0000000738200000, 0x00000007aab00000)
object space 919552K, 0% used [0x0000000700000000,0x00000007000f5bc8,0x0000000738200000)
Metaspace used 410659K, capacity 455416K, committed 455424K, reserved 456704K
从上面的元空间详细信息来看,在 GC 之前,JVM 保留了 456704K 并提交了 455424K 用于元数据分配。元空间使用量为 410659K。这里需要注意的一件有趣的事情是容量,它充当元空间的内部高水位线。当在该阈值内无法满足特定元数据分配请求时,将调用 GC 来清理并在元空间中腾出一些可用空间。而且,如果该 GC 无法提供足够的可用空间,则会提交更多内存并增加容量。在本例中,此阈值设置为 455416K,其中 410659K 已被使用。在导致此 Full GC 发生的容量限制内无法满足新的元数据分配。
GC之后,调整Metaspace的reserved、committed和capacity boundaries以满足元数据空间需求。我们可以在下一次 GC 事件之前记录的“Before GC”详细信息中找到新值。
{Heap before GC invocations=17 (full 1):
PSYoungGen total 93696K, used 1822K [0x00000007aab00000, 0x00000007b0e80000, 0x0000000800000000)
eden space 91136K, 2% used [0x00000007aab00000,0x00000007aacc7af0,0x00000007b0400000)
from space 2560K, 0% used [0x00000007b0680000,0x00000007b0680000,0x00000007b0900000)
to space 2560K, 0% used [0x00000007b0400000,0x00000007b0400000,0x00000007b0680000)
ParOldGen total 1172480K, used 1360K [0x0000000700000000, 0x0000000747900000, 0x00000007aab00000)
object space 1172480K, 0% used [0x0000000700000000,0x00000007001542b0,0x0000000747900000)
Metaspace used 685064K, capacity 759024K, committed 759040K, reserved 759808K
2021-10-07T14:13:11.843+0800: [GC (Metadata GC Threshold) [PSYoungGen: 1822K->704K(93696K)] 3183K->2064K(1266176K), 0.0021419 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
--- <snip> ---
2021-10-07T14:13:11.845+0800: [Full GC (Metadata GC Threshold) [PSYoungGen: 704K->0K(93696K)] [ParOldGen: 1360K->1989K(1497600K)] 2064K->1989K(1591296K), [Metaspace: 685064K->685064K(759808K)], 0.0501529 secs] [Times: user=0.05 sys=0.00, real=0.05 secs]
上面的 Metaspace 日志条目显示,在 GC 调用 #16 之后,保留空间和提交空间分别增加到 759808K 和 759040K。高水位线也提高到 759024K,自上次 GC 以来使用量从 410659K 增加到 685064K。
压缩类空间
我们也可以有一个单独的空间作为元空间的一部分,只存储元数据的类部分。这个单独的空间称为压缩类空间,其中的类部分元数据使用 Java 对象的 32 位偏移量进行引用。Thomas Stuefe 的博客文章中对压缩类空间的解释非常漂亮:https://stuefe.de/posts/metaspace/what-is-compressed-class-space/
在上一节中,为了简化日志,我使用选项-XX:-UseCompressedClassPointers禁用了单独的压缩类空间。在 64 位平台上,压缩类空间默认启用,默认保留空间大小为 1 GB。Compressed类空间的预留空间是在JVM初始化时设置的,以后不能改变其大小。Hotspot JVM 允许压缩类空间的最大保留空间大小为 3GB,可以使用 JVM 选项-XX:CompressedClassSpaceSize=n进行配置。
压缩类空间使用信息
现在,让我们看一下使用同一程序生成的一些日志记录,但启用了单独的压缩类空间作为元空间的一部分。
{Heap before GC invocations=6 (full 3):
--- <snip> ---
Metaspace used 20669K, capacity 25846K, committed 35456K, reserved 1077248K
class space used 1243K, capacity 2086K, committed 8064K, reserved 1048576K
2021-10-09T07:23:16.585+0800: [Full GC (Metadata GC Threshold) [PSYoungGen: 96K->0K(76288K)] [ParOldGen: 410K->446K(219136K)] 506K->446K(295424K), [Metaspace: 20669K->20669K(1077248K)], 0.0035401 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
上面的前两个日志显示元空间的总使用量为 20669K,其中类空间使用了 1243K。同样,元空间总容量 (HWM) 为 25846K,其中类空间容量 (HWM) 在此次 GC 调用时为 2086K。重申一下,容量(capacity)是一个阈值,当使用率接近容量时,就会调用 GC。
在这种情况下,元空间提交的大小为 35456K,并且从该空间,类空间提交了 8064K。这意味着 35456K-8064K=27392K 是元数据非类部分的承诺空间。JVM 选项MaxMetaspaceSize可用于设置总提交大小的最大限制。
MaxMetaspaceSize = Max limit on the committed size of Metaspace
= Max limit on the (committed size of non-class-part of metaspace +
committed size of class space)
至于预留空间,我们可以看到类空间有1GB(1048576K)的虚拟预留空间(默认),总元空间(包括类空间)有1077248K预留空间。
java.lang.OutOfMemoryError
重要的是要了解我们的应用程序的类和非类元数据的空间要求,并相应地调整元空间的大小。MaxMetaspaceSize JVM 选项设置了 Metaspace 提交大小的上限,如果配置得不够大,可能会导致java.lang.OutOfMemoryError: Metaspace。另外,如前所述,为压缩类空间配置的所有空间都是预先保留的,以后在运行时不能增长。因此,如果 Compressed class space 的预留空间没有配置足够大,我们可能会遇到java.lang.OutOfMemoryError: Compressed class space failure。
碎片化
请注意,由于元空间中的碎片(在元空间的非类部分和类部分中),我们也可能会遇到 OutOfMemoryError 失败。让我们看几个指示元空间中的碎片的示例。
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
Heap
PSYoungGen total 166912K, used 6574K [0x000000076ab00000, 0x0000000775a80000, 0x00000007c0000000)
eden space 164352K, 4% used [0x000000076ab00000,0x000000076b16ba08,0x0000000774b80000)
from space 2560K, 0% used [0x0000000774e00000,0x0000000774e00000,0x0000000775080000)
to space 2560K, 0% used [0x0000000774b80000,0x0000000774b80000,0x0000000774e00000)
ParOldGen total 2522624K, used 4065K [0x00000006c0000000, 0x0000000759f80000, 0x000000076ab00000)
object space 2522624K, 0% used [0x00000006c0000000,0x00000006c03f8560,0x0000000759f80000)
Metaspace used 1751795K, capacity 2097086K, committed 2097132K, reserved 2977792K
class space used 94384K, capacity 168958K, committed 168960K, reserved 1048576K
上面的日志记录显示Java程序失败,出现java.lang.OutOfMemoryError: Metaspace error。从元空间和类空间的使用细节可以看出,元空间的非类部分的承诺大小和容量分别为(2097132K-168960K=)1928172K和(2097086K-168958K=)1928128K,而只有1657411K是被非类元数据使用。因此,在 Metaspace 的非类部分显然有大量可用空间,但应用程序仍然失败并出现 OutOfMemoryError,这表明 Metaspace 中存在碎片。
同样,以下日志显示类空间使用量为 24868K,而提交的类空间大小为 1048576K,表明类空间中有足够的可用空间,但类空间的分配仍然失败并出现 OutOfMemoryError。
Exception in thread "main" java.lang.OutOfMemoryError: Compressed class space
Heap
PSYoungGen total 894959616, used 0 [0x0000000780000000, 0x00000007c0000000, 0x00000007c0000000)
eden space 699392K, 0% used [0x0000000780000000,0x0000000780000000,0x00000007aab00000)
from space 174592K, 0% used [0x00000007aab00000,0x00000007aab00000,0x00000007b5580000)
to space 174592K, 0% used [0x00000007b5580000,0x00000007b5580000,0x00000007c0000000)
ParOldGen total 3221225472, used 273448976 [0x00000006c0000000, 0x0000000780000000, 0x0000000780000000)
object space 3145728K, 8% used [0x00000006c0000000,0x00000006d04c8010,0x0000000780000000)
Metaspace used 231029K, capacity 248352K, committed 1454592K, reserved 1456128K
class space used 24868K, capacity 30973K, committed 1048576K, reserved 1048576K
}
增强版JDK-8198423解决了 Metaspace 中的碎片问题。它集成到 Java 11 中。由于禁止单独类空间的元空间是无限的,为了避免在使用 Java 8 运行时由于碎片导致的 OutOfMemoryError 失败,在没有压缩类空间的情况下运行可能会有所帮助。请注意,此配置将导致 Java 堆空间要求增加,因为没有使用 64 位 klass 指针而不是 32 位偏移量的压缩类空间 Java 对象引用类元数据。
原文:https://poonamparhar.github.io/understanding-metaspace-gc-logs/