Java虚拟机-元空间

在HotSpot JVM(jvm 8以前)中,永久代中用于存放类和方法的元数据以及常量池,在Java中对应能通过反射获取到的数据,比如Class和Method。每当一个类初次被加载的时候,它的元数据都会放到永久代中。永久代是有大小限制的(启动时设置),因此如果加载的类太多,很有可能导致永久代内存溢出(java.lang.OutOfMemoryError: PermGen)。Java 8为了解决这一问题,彻底将永久代移除出了HotSpot JVM,将其原有的数据迁移至Java Heap或Metaspace(见参考【1】)。

什么是元空间?

元空间是 VM 用于存储类元数据的内存。类元数据是 JVM 进程中 Java 类的运行时表示形式 - 基本上是 JVM 处理 Java 类所需的任何信息。这包括但不限于 JVM 类文件格式中数据的运行时表示。

  • Klass 结构,Java 类运行时状态的 VM 内部表示。这包括 vtable 和 itable。
  • 方法元数据 - 运行时等效于类文件中的method_info,包含字节码异常表常量等内容。
  • 常量池
  • 注解
  • 方法计数器,记录方法被执行的次数,用来辅助 JIT 决策;
  • 其他

从存储上,大致可以分为【class空间】(只存储Kclass)和【非class空间】。注意,在不开启类空间压缩时,没有【class空间】,Klass在【非class空间】中存储。

虽然每个Java类都关联了一个 java.lang.Class 的实例,但它是一个贮存在堆中的 Java 对象。类的类元数据不是一个 Java 对象,它不在堆中,而是在 Metaspace 中。

当一个类被加载并且它在JVM中的运行时表示正在准备时,Metaspace由它的类加载器分配来存储该类的元数据。

为类分配的Metaspace归其类加载器所有。它仅在卸载该类加载器本身时释放,而不是之前。只有在这个加载器加载的所有类不再有活动实例,并且没有对这些类及其类加载器的引用,并且GC确实运行后,才会发生这种情况

但是,“释放Metaspace”并不一定意味着内存返回给操作系统。该内存的全部或一部分可以保留在JVM中;它可能会被重用以供将来的类加载,但当前它在JVM进程中未被使用。这部分的大小主要取决于Metaspace的碎片程度——Metaspace的已使用部分和空闲部分的交错程度。此外,Metaspace的一部分(压缩类空间)根本不会返回给操作系统。

Metaspace大小

有两个参数可以限制Metaspace大小:

  • -XX:MaxMetaspaceSize 确定允许Metaspace的最大提交阈值。默认情况下它是无限的。
  • -XX:CompressedClassSpaceSize 确定Metaspace的压缩类空间的虚拟大小(前提是能开启klass压缩指针)。它的默认值是1G(注意:这部分内存是reserved,不是commited)。

Metaspace 和 GC

Metaspace 只在 GC 运行并且卸载类加载器的时候才会释放空间。当然,在某些时候,需要主动触发 GC 来回收一些没用的 class metadata,即使这个时候对于堆空间来说,还达不到 GC 的条件。

Metaspace 可能在两种情况下触发 GC:

  • 分配空间时:虚拟机维护了一个阈值,如果 Metaspace 的空间大小超过了这个阈值,那么在新的空间分配申请时,虚拟机首先会通过收集可以卸载的类加载器来达到复用空间的目的,而不是扩大 Metaspace 的空间,这个时候会触发 GC。这个阈值会上下调整,和 Metaspace 已经占用的操作系统内存保持一个距离。
  • 碰到 Metaspace OOM:Metaspace 的总使用空间达到了 MaxMetaspaceSize 设置的阈值,或者 Compressed Class Space 被使用光了,如果这次 GC 真的通过卸载类加载器腾出了很多的空间,这很好,否则的话,我们会进入一个糟糕的 GC 周期,即使我们有足够的堆内存。

Metaspace 架构

与大多数其他重要的分配器一样,Metaspace是分层实现的。

  • 在底部,内存从操作系统分配大region
  • 在中间,我们将这些region切分成不那么大的chunk并将它们交给类加载器。
  • 在顶部,类加载器切割这些块以服务调用者代码。

底层:空间列表

在最底层,JVM 先申请虚拟内存(MEM_RESERVE),然后按需要通过 mmap(3) 接口向操作系统提交物理内存(MEM_COMMIT),在 64 位平台上,每次申请 2MB 空间。每次申请过来的内存区域,作为一个 Node放到全局链表中 VirtualSpaceLis

  • 每个Node内部需要维护一个高水位线标记,将已提交的空间和仍未提交的空间分开。当 Node 内已提交内存快达到水位线的时候,向操作系统commit新的内存页。并且相应地提高水位线。保留一点空白以避免过于频繁地调用操作系统。
  • 这种情况一直持续到Node被完全用完,然后,一个新的节点会被申请,并加入链表中。旧节点被退役。

内存是从一个名为MetaChunk的节点分配的。它们有三种大小,分别命名为专用、小型和中型——命名是历史性的——通常大小为1K/4K/64K(受是否32/64位VM,是否是压缩区域影响)。

因此,VirtualSpaceList及其节点是全局结构,而Metachunk属于一个类加载器。VirtualSpaceList中的单个节点可能并且经常包含来自不同类加载器的chunk:

当类加载器及其所有关联的类被卸载时,用于保存其类元数据的Metaspace被释放。所有空闲块都被添加到全局空闲列表(块管理器):

这些块可以被重用:如果另一个类加载器开始加载类并分配Metaspace,它可能会得到一个空闲块,而不是分配一个新块。

中间层 元数据块

当一个类加载器向Metaspace请求一块元数据(通常是少量的——几十或几百字节)的内存时(比如200字节),它会得到一个Metachunk——一块通常比它请求的大得多的内存。

参考