Advanced-Java.02b1.MetaSpace解析

Metaspace 中有什么?

Metaspace 分为 Klass 和 Non-Klass 两个区域:

  • Klass:每个 Java 类都对应一个 Klass(是一个 C++对象),Klass 里主要保存了类的 vtable 和 itable 和其他,这部分数据大都是指针类型,例如 vtable 保存了指向方法的指针;
  • Non-Klass:这部分可以理解为 Klass 的数据区,存储了比较大的元数据,主要包括了常量池(constant pool)、字节码等等 @enhanced

在 Heap 中,每个 Java 类都有一个 Class 对象(区分上面的 Klass),这是一个 Java 对象,从.class 文件加载出的大多数数据都保存在 MetaSpace(如上所说),所以 Heap 中的 Class 对象可以认为是一个“与 Klass 交互的接口”;

此外,每次 new 都会在 Heap 创建类的对象(Object),每个对象在内存中都有一个“对象头”,包括 Mark Word 和指向 Klass 对象的指针,在 64 位机器上,指针默认大小是 64bit,所以会占用很大的内存,可以开启压缩指针减少内存消耗(开启后这个 Klass 指针就是 32bits),32 位指针最多能管理的 2^32 = 4G 内存,所以开启压缩指针的情况下,MetaSpace 中的 Klass 区也不能超过这个 4G 上限,否则 32 位指针无法管理这么大的内存区。

JVM 提供了 -XX:UseCompressedClassPointers 来指定是否开启压缩指针,只有在开启这个参数的情况下,Metaspace 才会有 Klass 区
默认情况下这个参数是打开的,如果我们把-Xmx 设置大于 32G 的话,因为内存很充足,所以就没必要开压缩指针,MetaSpace 中也就不再有Klass 区了;

下图是堆中的 Object 对象、Metaspace 中的 Klass 对象、Non-Klass 对象之间的关系:
../_images/Advanced-Java.02b1.MetaSpace解析-2023-05-23-1.png
【图】来自 全网最硬核 JVM 内存解析 - 7.元空间存储的元数据 - 掘金

Klass:其实就是 Java 类的实例(每个 Java 的 class 有一个对应的对象实例,用来反射访问,这个就是那个对象实例),即 Java 对象头的类型指针指向的实例:

Non-Klass:

  • Symbol:符号常量,即类中所有的符号字符串,例如类名称,方法名称,方法定义等等。
  • ConstantPool:运行时常量池,数据来自于类文件中的常量池。
  • ConstanPoolCache:运行时常量池缓存,用于加速常量池访问
  • ConstMethod:类文件中的方法解析后,静态信息放入 ConstMethod,这部分信息可以理解为是不变的,例如字节码,行号,方法异常表,本地变量表,参数表等等。
  • MethodCounters:方法的计数器相关数据。
  • MethodData:方法数据采集,动态编译相关数据。例如某个方法需要采集一些指标,决定是否采用 C1 C2 动态编译优化性能。
  • Method:Java 方法,包含以上 ConstMethodMethodCountersMethodData 的指针以及一些额外数据。
  • RecordComponent:对应 Java 14 新特性 Record,即从 Record 中解析出的关键信息。

注意,老版本中, UseCompressedClassPointers 取决于 UseCompressedOops,即压缩对象指针如果没开启,那么压缩类指针也无法开启。但是从 Java 15 Build 23 开始, UseCompressedClassPointers 已经不再依赖 UseCompressedOops 了,两者在大部分情况下已经独立开来。除非在 x86 的 CPU 上面启用 JVM Compiler Interface(例如使用 GraalVM)。

vtable & itable

➤ vtable 是 Java 虚拟函数表,Klass 通过虚函数表 vtable,来实现运行期的方法分派,也可以叫动态绑定;
C++对象中的 vtable 只有虚函数,但 Java 中没有虚函数一说,所有函数都是动态绑定的,JVM 中的 Klass 的 vtable 包括所有函数(static、final 除外);
Java 的 vtable 与 C++的另一个区别:C++是每个对象都有 vtable,Java 只在 Klass 对象保存了 vtable;

➤ itable 是 Java 接口函数表,为了方便查找某个接口对应的方法实现。itable 的结构比 vtable 复杂,除了记录方法地址外还得记录该方法所属的接口类 klass。

more ref: 第6.3篇-klassVtable与klassItable类的介绍 - 鸠摩(马智) - 博客园

Metaspace 中类加载和卸载

➤ Metaspace 中的元数据(Klass & Non-Klass)何时被加载?
在 ClassLoader 加载.class,在“准备”的阶段,类的元数据被载入 MetaSpace,这些元数据归加载它们的 ClassLoader 所有;

../_images/Advanced-Java.02b1.MetaSpace解析-2023-05-23-2.png

➤ Metaspace 中的元数据何时卸载?
在 GC 时,当 ClassLoader 加载的全部对象都被标为“不活动”,那么接下来这个 ClassLoader 对象也会被释放,最后会卸载掉 MetaSpace 中的元数据(参见:jls12.7-类和接口的卸载)

../_images/Advanced-Java.02b1.MetaSpace解析-2023-05-23-3.png
【图】来自 https://javakk.com/391.html

Metaspace 相关的 VM 参数

MetaspaceSize
默认 20.8M 左右(x86 下开启 c2 模式),主要是控制 metaspaceGC 发生的初始阈值,也是最小阈值,但是触发 metaspaceGC 的阈值是不断变化的,与之对比的主要是指 Klass Metaspace 与 NoKlass Metaspace 两块 committed 的内存和。

MaxMetaspaceSize
默认基本是无穷大,但是我还是建议大家设置这个参数,因为很可能会因为没有限制而导致 metaspace 被无止境使用(一般是内存泄漏)而被 OS Kill。这个参数会限制 metaspace(包括了 Klass Metaspace 以及 NoKlass Metaspace)被 committed 的内存大小,会保证 committed 的内存不会超过这个值,一旦超过就会触发 GC,这里要注意和 MaxPermSize 的区别,MaxMetaspaceSize 并不会在 jvm 启动的时候分配一块这么大的内存出来,而 MaxPermSize 是会分配一块这么大的内存的。

CompressedClassSpaceSize
默认 1G,这个参数主要是设置 Klass Metaspace 的大小,不过这个参数设置了也不一定起作用,前提是能开启压缩指针,假如-Xmx 超过了 32G,压缩指针是开启不来的。如果有 Klass Metaspace,那这块内存是和 Heap 连着的。

jstat 中 Metaspace 字段

  • MC 表示 Klass Metaspace 以及 NoKlass Metaspace 两者总共 committed 的内存大小,单位是 KB,虽然从上面的定义里我们看到了是 capacity,但是实质上计算的时候并不是 capacity,而是 committed
  • MU 表示是 Klass Metaspace 以及 NoKlass Metaspace 两者已经使用了的内存大小
  • CCSC 表示的是 Klass Metaspace 的已经被 commit 的内存大小,单位也是 KB
  • CCSU 表示 Klass Metaspace 的已经被使用的内存大小
  • MCMX 表示 Klass Metaspace 以及 NoKlass Metaspace 两者总共的 reserved 的内存大小,比如默认情况下 Klass Metaspace 是通过 CompressedClassSpaceSize 这个参数来 reserved 1G 的内存,NoKlass Metaspace 默认 reserved 的内存大小是 2x InitialBootClassLoaderMetaspaceSize
  • CCSMX 表示 Klass Metaspace reserved 的内存大小

区分内存分配的 reserve & commit:

  • reserve 动作:只是向 OS 申请内存,但 OS 并没有实际分配物理内存,对应 OpenJDK 的 os::reserve_memory 函数,reserve 最终是通过 mmap(2) 实现的;
  • commit 动作:从已经 reserved 的内存区域中,commit 一部分出来,同时 commited 的内存也可以 uncommit 释放,对应 OpenJDK os::commit_memory 函数;
  • commit 出来的内存也是没有分配物理内存的,真正的分配物理内存要等到向这块内存写入的时候;

Reference