likes
comments
collection
share

全网最硬核 JVM 内存解析 - 8.元空间的核心概念与设计

作者站长头像
站长
· 阅读数 14

个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判。如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 issue,谢谢支持~ 另外,本文为了避免抄袭,会在不影响阅读的情况下,在文章的随机位置放入对于抄袭和洗稿的人的“亲切”的问候。如果是正常读者看到,笔者在这里说声对不起,。如果被抄袭狗或者洗稿狗看到了,希望你能够好好反思,不要再抄袭了,谢谢。 今天又是干货满满的一天,这是全网最硬核 JVM 解析系列第四篇,往期精彩:

本篇是关于 JVM 内存的详细分析。网上有很多关于 JVM 内存结构的分析以及图片,但是由于不是一手的资料亦或是人云亦云导致有很错误,造成了很多误解;并且,这里可能最容易混淆的是一边是 JVM Specification 的定义,一边是 Hotspot JVM 的实际实现,有时候人们一些部分说的是 JVM Specification,一部分说的是 Hotspot 实现,给人一种割裂感与误解。本篇主要从 Hotspot 实现出发,以 Linux x86 环境为主,紧密贴合 JVM 源码并且辅以各种 JVM 工具验证帮助大家理解 JVM 内存的结构。但是,本篇仅限于对于这些内存的用途,使用限制,相关参数的分析,有些地方可能比较深入,有些地方可能需要结合本身用这块内存涉及的 JVM 模块去说,会放在另一系列文章详细描述。最后,洗稿抄袭狗不得 house

本篇全篇目录(以及涉及的 JVM 参数):

  1. 从 Native Memory Tracking 说起(全网最硬核 JVM 内存解析 - 1.从 Native Memory Tracking 说起开始)
    1. Native Memory Tracking 的开启
    2. Native Memory Tracking 的使用(涉及 JVM 参数:NativeMemoryTracking
    3. Native Memory Tracking 的 summary 信息每部分含义
    4. Native Memory Tracking 的 summary 信息的持续监控
    5. 为何 Native Memory Tracking 中申请的内存分为 reserved 和 committed
  2. JVM 内存申请与使用流程(全网最硬核 JVM 内存解析 - 2.JVM 内存申请与使用流程开始)
    1. Linux 下内存管理模型简述
    2. JVM commit 的内存与实际占用内存的差异
      1. JVM commit 的内存与实际占用内存的差异
    3. 大页分配 UseLargePages(全网最硬核 JVM 内存解析 - 3.大页分配 UseLargePages开始)
      1. Linux 大页分配方式 - Huge Translation Lookaside Buffer Page (hugetlbfs)
      2. Linux 大页分配方式 - Transparent Huge Pages (THP)
      3. JVM 大页分配相关参数与机制(涉及 JVM 参数:UseLargePages,UseHugeTLBFS,UseSHM,UseTransparentHugePages,LargePageSizeInBytes
  3. Java 堆内存相关设计(全网最硬核 JVM 内存解析 - 4.Java 堆内存大小的确认开始)
    1. 通用初始化与扩展流程
    2. 直接指定三个指标的方式(涉及 JVM 参数:MaxHeapSize,MinHeapSize,InitialHeapSize,Xmx,Xms
    3. 不手动指定三个指标的情况下,这三个指标(MinHeapSize,MaxHeapSize,InitialHeapSize)是如何计算的
    4. 压缩对象指针相关机制(涉及 JVM 参数:UseCompressedOops)(全网最硬核 JVM 内存解析 - 5.压缩对象指针相关机制开始)
      1. 压缩对象指针存在的意义(涉及 JVM 参数:ObjectAlignmentInBytes
      2. 压缩对象指针与压缩类指针的关系演进(涉及 JVM 参数:UseCompressedOops,UseCompressedClassPointers
      3. 压缩对象指针的不同模式与寻址优化机制(涉及 JVM 参数:ObjectAlignmentInBytes,HeapBaseMinAddress
    5. 为何预留第 0 页,压缩对象指针 null 判断擦除的实现(涉及 JVM 参数:HeapBaseMinAddress
    6. 结合压缩对象指针与前面提到的堆内存限制的初始化的关系(涉及 JVM 参数:HeapBaseMinAddress,ObjectAlignmentInBytes,MinHeapSize,MaxHeapSize,InitialHeapSize
    7. 使用 jol + jhsdb + JVM 日志查看压缩对象指针与 Java 堆验证我们前面的结论
      1. 验证 32-bit 压缩指针模式
      2. 验证 Zero based 压缩指针模式
      3. 验证 Non-zero disjoint 压缩指针模式
      4. 验证 Non-zero based 压缩指针模式
    8. 堆大小的动态伸缩(涉及 JVM 参数:MinHeapFreeRatio,MaxHeapFreeRatio,MinHeapDeltaBytes)(全网最硬核 JVM 内存解析 - 6.其他 Java 堆内存相关的特殊机制开始)
    9. 适用于长期运行并且尽量将所有可用内存被堆使用的 JVM 参数 AggressiveHeap
    10. JVM 参数 AlwaysPreTouch 的作用
    11. JVM 参数 UseContainerSupport - JVM 如何感知到容器内存限制
    12. JVM 参数 SoftMaxHeapSize - 用于平滑迁移更耗内存的 GC 使用
  4. JVM 元空间设计(全网最硬核 JVM 内存解析 - 7.元空间存储的元数据开始)
    1. 什么是元数据,为什么需要元数据
    2. 什么时候用到元空间,元空间保存什么 4.2.1. 什么时候用到元空间,以及释放时机 4.2.2. 元空间保存什么
    3. 元空间的核心概念与设计(全网最硬核 JVM 内存解析 - 8.元空间的核心概念与设计开始)
      1. 元空间的整体配置以及相关参数(涉及 JVM 参数:MetaspaceSize,MaxMetaspaceSize,MinMetaspaceExpansion,MaxMetaspaceExpansion,MaxMetaspaceFreeRatio,MinMetaspaceFreeRatio,UseCompressedClassPointers,CompressedClassSpaceSize,CompressedClassSpaceBaseAddress,MetaspaceReclaimPolicy
      2. 元空间上下文 MetaspaceContext
      3. 虚拟内存空间节点列表 VirtualSpaceList
      4. 虚拟内存空间节点 VirtualSpaceNodeCompressedClassSpaceSize
      5. MetaChunk
        1. ChunkHeaderPool 池化 MetaChunk 对象
        2. ChunkManager 管理空闲的 MetaChunk
      6. 类加载的入口 SystemDictionary 与保留所有 ClassLoaderDataClassLoaderDataGraph
      7. 每个类加载器私有的 ClassLoaderData 以及 ClassLoaderMetaspace
      8. 管理正在使用的 MetaChunkMetaspaceArena
      9. 元空间内存分配流程(全网最硬核 JVM 内存解析 - 9.元空间内存分配流程开始)
        1. 类加载器到 MetaSpaceArena 的流程
        2. MetaChunkArena 普通分配 - 整体流程
        3. MetaChunkArena 普通分配 - FreeBlocks 回收老的 current chunk 与用于后续分配的流程
        4. MetaChunkArena 普通分配 - 尝试从 FreeBlocks 分配
        5. MetaChunkArena 普通分配 - 尝试扩容 current chunk
        6. MetaChunkArena 普通分配 - 从 ChunkManager 分配新的 MetaChunk
        7. MetaChunkArena 普通分配 - 从 ChunkManager 分配新的 MetaChunk - 从 VirtualSpaceList 申请新的 RootMetaChunk
        8. MetaChunkArena 普通分配 - 从 ChunkManager 分配新的 MetaChunk - 将 RootMetaChunk 切割成为需要的 MetaChunk
        9. MetaChunk 回收 - 不同情况下, MetaChunk 如何放入 FreeChunkListVector
      10. ClassLoaderData 回收
    4. 元空间分配与回收流程举例(全网最硬核 JVM 内存解析 - 10.元空间分配与回收流程举例开始)
      1. 首先类加载器 1 需要分配 1023 字节大小的内存,属于类空间
      2. 然后类加载器 1 还需要分配 1023 字节大小的内存,属于类空间
      3. 然后类加载器 1 需要分配 264 KB 大小的内存,属于类空间
      4. 然后类加载器 1 需要分配 2 MB 大小的内存,属于类空间
      5. 然后类加载器 1 需要分配 128KB 大小的内存,属于类空间
      6. 新来一个类加载器 2,需要分配 1023 Bytes 大小的内存,属于类空间
      7. 然后类加载器 1 被 GC 回收掉
      8. 然后类加载器 2 需要分配 1 MB 大小的内存,属于类空间
    5. 元空间大小限制与动态伸缩(全网最硬核 JVM 内存解析 - 11.元空间分配与回收流程举例开始)
      1. CommitLimiter 的限制元空间可以 commit 的内存大小以及限制元空间占用达到多少就开始尝试 GC
      2. 每次 GC 之后,也会尝试重新计算 _capacity_until_GC
    6. jcmd VM.metaspace 元空间说明、元空间相关 JVM 日志以及元空间 JFR 事件详解(全网最硬核 JVM 内存解析 - 12.元空间各种监控手段开始)
      1. jcmd <pid> VM.metaspace 元空间说明
      2. 元空间相关 JVM 日志
      3. 元空间 JFR 事件详解
        1. jdk.MetaspaceSummary 元空间定时统计事件
        2. jdk.MetaspaceAllocationFailure 元空间分配失败事件
        3. jdk.MetaspaceOOM 元空间 OOM 事件
        4. jdk.MetaspaceGCThreshold 元空间 GC 阈值变化事件
        5. jdk.MetaspaceChunkFreeListSummary 元空间 Chunk FreeList 统计事件
  5. JVM 线程内存设计(重点研究 Java 线程)(全网最硬核 JVM 内存解析 - 13.JVM 线程内存设计开始)
    1. JVM 中有哪几种线程,对应线程栈相关的参数是什么(涉及 JVM 参数:ThreadStackSize,VMThreadStackSize,CompilerThreadStackSize,StackYellowPages,StackRedPages,StackShadowPages,StackReservedPages,RestrictReservedStack
    2. Java 线程栈内存的结构
    3. Java 线程如何抛出的 StackOverflowError
      1. 解释执行与编译执行时候的判断(x86为例)
      2. 一个 Java 线程 Xss 最小能指定多大

4. JVM 元空间设计

4.3. 元空间的核心概念与设计

4.3.1. 元空间的整体配置以及相关参数

元空间配置相关的参数:

  • MetaspaceSize:初始元空间大小,也是最小元空间大小。后面元空间大小伸缩的时候,不会小于这个大小。默认是 21M。抄袭剽窃侵权 滚
  • MaxMetaspaceSize:最大元空间大小,默认是无符号 int 最大值。
  • MinMetaspaceExpansion:每次元空间大小伸缩的时候,至少改变的大小。默认是 256K。后文讲到元空间内存大小限制的时候会详细分析。
  • MaxMetaspaceExpansion:每次元空间大小伸缩的时候,最多改变的大小。默认是 4M。后文讲到元空间内存大小限制的时候会详细分析。
  • MaxMetaspaceFreeRatio:最大元空间空闲比例,默认是 70,即 70%。后文讲到元空间内存大小限制的时候会详细分析。
  • MinMetaspaceFreeRatio:最小元空间空闲比例,默认是 40,即 40%。后文讲到元空间内存大小限制的时候会详细分析。
  • UseCompressedClassPointers:前文提到过,是否开启压缩类指针。默认是开启的。老版本中, UseCompressedClassPointers 取决于 UseCompressedOops,即压缩对象指针如果没开启,那么压缩类指针也无法开启。但是从 Java 15 Build 23 开始, UseCompressedClassPointers 已经不再依赖 UseCompressedOops,两者在大部分情况下已经独立开来。除非在 x86 的 CPU 上面启用 JVM Compiler Interface(例如使用 GraalVM)。参考 JDK ISSUE:bugs.openjdk.java.net/browse/JDK-…
  • CompressedClassSpaceSize:如果启用了压缩类指针,则元空间会分为类元空间和数据元空间,否则只有数据元空间。这个参数限制类元空间的大小,范围是 1M ~ 3G。默认大小是 1G,如果指定了 MaxMetaspaceSize,那么为 1G 与 MaxMetaspaceSize * 0.8 中比较小的那个值,
  • CompressedClassSpaceBaseAddress:类元空间起始虚拟内存地址,这个一般不指定。作用和前文分析堆内存的堆起始位置的作用差不多。
  • MetaspaceReclaimPolicy:可以为:balanced, aggressive, 以及 none,需要注意一点的是 none 要被移除了(bugs.openjdk.org/browse/JDK-…)。默认是 balanced。具体主要是影响元空间底层相关的配置,下面我们会详细分析。

元空间底层相关的配置包括:

  • commit 粒度 - commit_granule:通过第二章的分析我们知道,JVM 的空间一般是先 reserve, 之后 commit 之前 reserve 的空间的一部分,然后才能使用的。这个 commit 粒度代表元空间中 commit 内存的最小粒度,元空间在扩容缩容的时候最小的大小单位是 commit 粒度。
  • 虚拟内存空间节点内存大小 - virtual_space_node_default_word_size:这是后文我们会详细分析的 VirtualSpaceNode 的虚拟内存大小。大小在 64 位环境下是 64 MB。
  • 虚拟内存空间节点内存对齐 - virtual_space_node_reserve_alignment_words:这是后文我们会详细分析的 VirtualSpaceNode 的虚拟内存大小需要对齐的大小,即整体大小需要大于这个对齐大小并且是这个对齐大小整数倍。这个大小就是 MetaChunk 的最大大小,即 4MB。
  • 当前 MetaChunk 不足以分配的时候,是否尝试扩容当前 MetaChunk - enlarge_chunks_in_place:这个参数在正式 JVM 中是 true,并且不能修改。后文我们会详细分析什么是 MetaChunk。这里简单理解就是,元空间整体使用了和 Linux 伙伴分配算法类似的设计与抽象,其中内存分配的单元就是 Chunk,元空间中对应的就是 MetaChunk。
  • 分配新的 MetaChunk 的时候,是否一下子 commit MetaChunk 所有的内存 - new_chunks_are_fully_committed:后文我们会详细分析什么是 MetaChunk
  • 在 MetaChunk 整个空间都没有使用的时候,是否将 MetaChunk 的内存全部释放回操作系统 - uncommit_free_chunks:后文我们会详细分析什么是 MetaChunk

从 Java 16 开始,引入了弹性元空间。老的元空间由于设计上分配粒度比较大,并且没有很好地释放空间的策略设计,所以占用可能比较大。Java 16 开始,JEP 387: Elastic Metaspace 引入了弹性元空间的设计,也是我们这里要讨论的设计。这个弹性元空间也引入了一个重要的参数 -XX:MetaspaceReclaimPolicy

MetaspaceReclaimPolicy:可以为:balanced, aggressive, 以及 none,需要注意一点的是 none 要被移除了(bugs.openjdk.org/browse/JDK-…),这三个配置具体影响是:

全网最硬核 JVM 内存解析 - 8.元空间的核心概念与设计

4.3.2. 元空间上下文 MetaspaceContext

MetaspaceContext 本身直接原生堆上面分配,Native Memory Tracking 中属于 Metaspace 那一类别,即元空间的抽象类占用的空间。

https://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/memory/metaspace/metaspaceContext.hpp

class MetaspaceContext : public CHeapObj<mtMetaspace>

JVM 元空间,会在全局建立两个元空间上下文MetaspaceContext),一个用于类元空间(我们后面称为类元空间 MetaspaceContext),一个用于数据元空间(我们后面称为数据元空间 MetaspaceContext)。当然,在没有启用压缩类指针的时候,只会初始化一个数据元空间 MetaspaceContext,不会初始化类元空间 MetaspaceContext,之后使用分配的时候,也只会用数据元空间 MetaspaceContext 进行分配。但是我们在后面讨论的时候,只会讨论开启压缩类指针的情况,因为这是默认并且常用的情况

全网最硬核 JVM 内存解析 - 8.元空间的核心概念与设计

每个 MetaspaceContext 都会对应一个独立的 VirtualSpaceList,以及一个独立的 ChunkManager

全网最硬核 JVM 内存解析 - 8.元空间的核心概念与设计

这个 VirtualSpaceList 中的每一个元素都是一个 VirtualSpaceNode。顾名思义,VirtualSpaceNode 是从操作系统申请内存,与元空间内存划分的抽象隔离的中间层抽象。VirtualSpaceList 负责与操作系统交互,申请或者释放内存。元空间与 VirtualSpaceList 交互,使用内存。

ChunkManager 顾名思义,是管理所有 Chunk 的内存管理器。Chunk 这个概念经常出现在各种伙伴内存管理算法框架(Buddy Allocator)中,一般指内存管理分配的最小单元,这里的 Chunk 抽象对应的就是 MetaChunkChunkManagerVirtualSpaceList 上面获取一块连续比较大的内存的 MetaChunk(其实是 RootMetaChunk),然后将这个 RootMetaChunk 按照分配需求,连续对半分割成需要的大小,返回这个合适大小的 MetaChunk,剩下的分割出来的 MetaChunk 进入 FreeChunkListVector 用于下次分配 MetaChunk 的时候,直接返回合适的,就不再从 VirtualSpaceList 获取了。

我们接下来仔细分析 VirtualSpaceListChunkManager

4.3.3. 虚拟内存空间节点列表 VirtualSpaceList

VirtualSpaceList 本身直接原生堆上面分配,Native Memory Tracking 中属于 Class 那一类别,即元空间的加载类占用的空间。其实本人感觉这么设计不太合理,应该和 MetaspaceContext 属于同一个类别才比较合理。真正分配加载的类的占用空间的是从 VirtualSpaceNode 上面标记的内存分配的,这是下一小节要分析的内容。

https://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/memory/metaspace/virtualSpaceList.hpp

class VirtualSpaceList : public CHeapObj<mtClass> 

首先提一点,类元空间 MetaspaceContext 与数据元空间 MetaspaceContext 略有不同:类元空间 MetaspaceContextVirtualSpaceList 是不可以扩展申请新的内存的,但是数据元空间 MetaspaceContextVirtualSpaceList 是可以的。也就是说:类元空间 MetaspaceContextVirtualSpaceList 其实只有一个 VirtualSpaceNode,但是数据元空间 MetaspaceContextVirtualSpaceList 是一个包含多个 VirtualSpaceNode 的列表。

全网最硬核 JVM 内存解析 - 8.元空间的核心概念与设计

4.3.4. 虚拟内存空间节点 VirtualSpaceNodeCompressedClassSpaceSize

VirtualSpaceNode 本身直接原生堆上面分配,Native Memory Tracking 中属于 Class 那一类别,即元空间的加载类占用的空间。其实本人感觉这么设计不太合理,应该和 MetaspaceContext 属于同一个类别才比较合理。真正分配加载的类的占用空间的是从 VirtualSpaceNode 上面标记的内存地址分配的,VirtualSpaceNode 本身的空间占用只是起到描述记录作用,应该也属于元空间描述的那一类。

https://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/memory/metaspace/virtualSpaceNode.hpp

class VirtualSpaceNode : public CHeapObj<mtClass>

VirtualSpaceNode 是一块连续的虚拟内存空间内存的抽象。类元空间的 VirtualSpaceList 只包含一个 VirtualSpaceNode,大小是前文提到的 CompressedClassSpaceSize

数据元空间并不像类元空间或者堆内存那样,一下子 reserve 最大堆内存限制的内存,而是每次 reserve VirtualSpaceNode 大小VirtualSpaceNode 大小在 64 位环境下是 64 MB:

https://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/memory/metaspace/metaspaceSettings.hpp

static const size_t _virtual_space_node_default_word_size =
      chunklevel::MAX_CHUNK_WORD_SIZE * NOT_LP64(2) LP64_ONLY(16); // 8MB (32-bit) / 64MB (64-bit)

VirtualSpaceNode 通过两个数据结构来管理它维护的虚拟内存空间:

  • CommitMask:实际是一个位图,用于维护哪些内存被 commit 了,哪些没有,位图的标记的单位就是前文提到的 commit_granule(commit 粒度)。
  • RootChunkAreaLUT:用于维护每个 RootMetaChunk 的内存分布。至于什么是 RootMetaChunk 在后续我们讲 MetaChunk 的时候会详细讲解。

一个 VirtualSpaceNode 的主要结构如下图所示: 全网最硬核 JVM 内存解析 - 8.元空间的核心概念与设计

4.3.5. MetaChunk

MetaChunk 是元空间内存分配的核心抽象,其本质就是描述一块连续的虚拟内存空间。MetaChunk 本身只是一个描述对象,它也是直接原生堆上面分配,Native Memory Tracking 中属于 Metaspace 那一类别,即元空间的抽象类占用的空间。这个描述对象是池化的,参考后面会分析的 ChunkHeaderPool。不要偷取他人的劳动成果!

元空间的任意分配,都是在某个 MetaChunk 上进行的(不要偷取他人的劳动成果!)。MetaChunk 有级别的概念,即 ChunkLevel,每个 MetaChunk 都有自己的 ChunkLevel,这个 ChunkLevel 主要代表了 MetaChunk 描述的内存空间的大小,每一个 level 都是下一个 level 大小的 2 倍:

全网最硬核 JVM 内存解析 - 8.元空间的核心概念与设计

VirtualSpaceNode 上直接划分的 MetaChunkRootMetaChunk,它的 ChunkLevel 为最高级别的 0,大小是 4MB,并且其中的内存只是 reserve 还没有 commit 的。

MetaChunk有三个状态:

  • Dead:即 MetaChunk 只是对象被创建出来,但是没有关联描述实际的虚拟内存。后面我们会知道,MetaChunk 是池化可回收在利用的,MetaChunk 的池就是 ChunkHeaderPool。位于 ChunkHeaderPool 都还没有关联描述实际的虚拟内存,状态为 Dead
  • Free:即 MetaChunk 关联描述了实际的虚拟内存,但是没有被实际使用。此时,这个 MetaChunk 位于 ChunkManager 管理。
  • InUse:即 MetaChunk 关联描述了实际的虚拟内存,也被实际使用了,此时,MetaChunkArena 管理这个 MetaChunk 上面的内存分配。

4.3.5.1. ChunkHeaderPool 池化 MetaChunk 对象

MetaChunk 实际上只是一块连续的虚拟内存空间的描述类(不要偷取他人的劳动成果!),即元数据类。由于类加载需要的大小不一,并且还经常会发生合并,切分等等,MetaChunk 可能有很多很多,元空间为了节省这个元数据类占用的空间,将其池化,回收再利用。这个池就是 ChunkHeaderPool。例如,从 VirtualSpaceNode 上直接划分 RootMetaChunk 的内存空间,会从 ChunkHeaderPool 申请一个 MetaChunk 用于描述。当两个 MetaChunk 的空间需要合并成一个的时候,其中一个 MetaChunk 其实就没有用了,会放回 ChunkHeaderPool,而不是直接 free 掉这个对象。

ChunkHeaderPool 本身直接原生堆上面分配,Native Memory Tracking 中属于 Metaspace 那一类别,即元空间的抽象类占用的空间。

https://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/memory/metaspace/chunkHeaderPool.hpp

class ChunkHeaderPool : public CHeapObj<mtMetaspace> 

其实从这里我们可以推测出,MetaChunk 本身也是直接原生堆上面分配,Native Memory Tracking 中也是属于 Metaspace 那一类别。

ChunkHeaderPool 的结构是:

全网最硬核 JVM 内存解析 - 8.元空间的核心概念与设计

其实 ChunkHeaderPool 的机制很简单:

  • 申请 MetaChunk 用于描述内存:
    • 首先查看 _freelist,是否有之前放回的 MetaChunk 可以使用,如果有,就返回那个 MetaChunk,并从 _freelist 移除这个 MetaChunk
    • 如果没有,读取 _current_slab 指向的 SlabSlab 核心就是一个预分配好的 MetaChunk 数组(大小是 128),_top 指的是当前使用到数组的哪一个。
    • 如果 _top 没有到 128,返回 _top 代表的 MetaChunk,并将 _top 加 1。
    • 如果 _top 到 128,创建新的 Slab_current_slab 指向这个新的 Slab
  • 回收 MetaChunk:放入 _freelist

4.3.5.2. ChunkManager 管理空闲的 MetaChunk

ChunkManager 本身直接原生堆上面分配,Native Memory Tracking 中属于 Metaspace 那一类别,即元空间的抽象类占用的空间。不要偷取他人的劳动成果!

class ChunkManager : public CHeapObj<mtMetaspace> 

https://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/memory/metaspace/chunkManager.hpp

ChunkManager 管理已经关联内存但是还没使用(状态是 Free)的 MetaChunk。在第一次从 VirtualSpaceNode 上面分配 RootMetaChunk 的内存的时候,根据申请的内存大小,决定要将 RootMetaChunk 拆分到某个 ChunkLevel 大小之后用于当前分配,拆分出来的其他的 MetaChunk 还没有使用,先放入一个类似于之前 ChunkHeaderPool 里面的 _free_list 的结构,用于下次申请 MetaChunk 用于分配的时候,先从这个里面找,找不到之后再从 VirtualSpaceNode 上面尝试分配新的 RootMetaChunk。不要惯着cao袭的人!

ChunkManager 的整体结构是:

全网最硬核 JVM 内存解析 - 8.元空间的核心概念与设计

ChunkManager 主要维护一个 FreeChunkListVectorFreeChunkListVector 里面是一个 FreeChunkList 数组(还有xigao dog 的码)。FreeChunkList 是一个 MetaChunk 链表,链表中都是 FreeMetaChunk,同样 ChunkLevelMetaChunk 位于同一个 FreeChunkList 中。FreeChunkList 数组以 ChunkLevel 为下标,这样的数据结构可以快速找到一个所需 ChunkLevelMetaChunkFreeChunkList这个链表其实是一个双向链表,包含头尾两个指针,如果一个 MetaChunk 管理的内存被 commit 了,就会放在链表头部,没有 commit 的放在链表尾部。

MetaChunk 具体的分配,切分,合并流程,我们会在介绍完 MetaspaceArena 之后详细分析。但是,MetaspaceArenaChunkManager 不一样,ChunkManager 是全局两个,一个属于类元空间,一个属于数据元空间,倘若没有开启压缩类指针,那么就只有一个数据元空间 ChunkManager,而 MetaspaceArena 我们后面会看到是每个 ClassLoader 独立私有的。所以,在讲 MetaspaceArena 之前,我们先要从另一个角度即 ClassLoader 加载类的角度出发,向下一层一层剖析到 MetaspaceArena

4.3.6. 类加载的入口 SystemDictionary 与保留所有 ClassLoaderDataClassLoaderDataGraph

类加载的入口在全局唯一的 SystemDictionary 中,这里我们只是为了看一下类加载需要哪些参数,来搞清楚对应关系,不用关心细节,入口代码是:

https://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/classfile/systemDictionary.cpp

InstanceKlass* SystemDictionary::resolve_from_stream(ClassFileStream* st,
                                                     Symbol* class_name,
                                                     Handle class_loader,
                                                     const ClassLoadInfo& cl_info,
                                                     TRAPS) {
  //隐藏类与普通类的加载方式不同,隐藏类是 JEP 371: Hidden Classes 引入的,Java 15 中发布的新特性
  if (cl_info.is_hidden()) {
    return resolve_hidden_class_from_stream(st, class_name, class_loader, cl_info, CHECK_NULL);
  } else {
    return resolve_class_from_stream(st, class_name, class_loader, cl_info, CHECK_NULL);
  }
}

可以看到,加载类需要以下参数:

  • ClassFileStream* st:类文件流
  • Symbol* class_name:加载的类的名称
  • Handle class_loader:是哪个类加载器
  • const ClassLoadInfo& cl_info:类加载器信息

在加载类的时候,SystemDictionary 会获取类加载器的 ClassLoaderDataClassLoaderData 是每个类加载器私有的。

https://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/classfile/systemDictionary.cpp

//通过类加载器获取对应的 `ClassLoaderData`
ClassLoaderData* SystemDictionary::register_loader(Handle class_loader, bool create_mirror_cld) {
  if (create_mirror_cld) {
    return ClassLoaderDataGraph::add(class_loader, true);
  } else {
    // 如果是 null,代表是 BootstrapClassLoader,使用全局的 BootstrapClassLoader 对应的 ClassLoaderData
    return (class_loader() == NULL) ? ClassLoaderData::the_null_class_loader_data() :
    //否则,从 ClassLoaderDataGraph 寻找或者创建 class_loader 对应的 ClassLoaderData
                                      ClassLoaderDataGraph::find_or_create(class_loader);
  }
}

ClassLoaderDataGraph 保存着所有的 ClassLoaderData,这个主要用来遍历每个类加载器,以及获取每个类加载器加载的类的信息,还有遍历类加载器加载的类,例如 jcmd 命令中的 VM.classloaders 以及 VM.classloader_stats 就是这么实现的。但是,我们就不纠结于 ClassLoaderDataGraph 的细节了,这不是咱们的重点。

4.3.7. 每个类加载器私有的 ClassLoaderData 以及 ClassLoaderMetaspace

ClassLoaderData 本身直接原生堆上面分配,Native Memory Tracking 中属于 Class 那一类别,即元空间的加载类占用的空间。这就很合理了,不加载类就不会有 ClassLoaderData

https://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/classfile/classLoaderData.hpp

    class ClassLoaderData : public CHeapObj<mtClass>

如前所述,ClassLoaderData 是每个类加载器私有的。ClassLoaderData 包含的元素众多,我们这里只关心它其中与元空间内存分配相关的,即 ClassLoaderMetaspace

https://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/classfile/classLoaderData.hpp

    ClassLoaderMetaspace * volatile _metaspace;

ClassLoaderMetaspace 本身直接原生堆上面分配,Native Memory Tracking 中属于 Class 那一类别,即元空间的加载类占用的空间。

https://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/memory/classLoaderMetaspace.hpp

    class ClassLoaderMetaspace : public CHeapObj<mtClass>

ClassLoaderMetaspace 有不同的类型(MetaspaceType):

  • MetaspaceType::StandardMetaspaceType:平台类加载器(Platform ClassLoader,Java 9 之前叫做 ext ClassLoader)以及应用类加载器(Application ClassLoader)的 ClassLoaderMetaspace
  • MetaspaceType::BootMetaspaceType:即根类加载器(Boostrap ClassLoader)的 ClassLoaderMetaspace
  • MetaspaceType::ClassMirrorHolderMetaspaceType:加载匿名类的类加载器的 ClassLoaderMetaspace
  • MetaspaceType::ReflectionMetaspaceType:反射调用的前几次通过 jni native 调用,超过一定次数会优化成生成字节码类调用。加载这些字节码类的类加载器是 jdk.internal.reflect.DelegatingClassLoader,这个类加载器的 ClassLoaderMetaspace 类型就是 ReflectionMetaspaceType

ClassLoaderMetaspaceMetaspaceContext 类似,如果压缩类指针开启,那么 ClassLoaderMetaspace 包含一个类元空间的 MetaspaceArena 和一个数据元空间的 MetaspaceArena,否则只有一个数据元空间的 MetaspaceArena

全网最硬核 JVM 内存解析 - 8.元空间的核心概念与设计

4.3.8. 管理正在使用的 MetaChunkMetaspaceArena

MetaspaceArena 本身直接原生堆上面分配,Native Memory Tracking 中属于 Class 那一类别,即元空间的加载类占用的空间。这也是肯定的,因为跟着类加载器存在

class MetaspaceArena : public CHeapObj<mtClass> 

MetaspaceArena 结构如下所示: 全网最硬核 JVM 内存解析 - 8.元空间的核心概念与设计

MetaspaceArena 包含:

  • 一个 MetachunkList:管理在该 MetaspaceArena 分配的 MetaChunk 的列表,列表的第一个是当前分配内存的 MetaChunk
  • 当前 MetaspaceArenaArenaGrowthPolicy:在当前分配内存的 MetaChunk 不够分配的时候,申请新的 MetaChunk 的大小。
  • Freeblocks: 在当前分配内存的 MetaChunk 不够分配的时候,需要分配新的 MetaChunk。当前的 MetaChunk 剩余空间放入 Freeblocks

Freeblocks 包含一个 BinList32 和一个 BlockTree。大小大于 33 字节的进入 BlockTree,否则进入 BinList32

BinList32 类似于 FreeChunkListVector,是一个链表的数组,同样大小的内存在同一数组下标的链表。

全网最硬核 JVM 内存解析 - 8.元空间的核心概念与设计

BlockTree 是一个在 Binary Search Tree(BST)的基础上,同样内存的节点在二叉树节点的后面形成链表的数据结构。

全网最硬核 JVM 内存解析 - 8.元空间的核心概念与设计

不同的类加载器类型的类元空间的 MetaspaceArena 与数据元空间的 MetaspaceArenaArenaGrowthPolicy 不同:

1.根类加载器(Boostrap ClassLoader)的 ClassLoaderMetaspace 类元空间的 MetaspaceArenaArenaGrowthPolicyMetachunkList每次增长都是申请大小为 256KMetaChunk

https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArenaGrowthPolicy.cpp

    static const chunklevel_t g_sequ_boot_class[] = {
        chunklevel::CHUNK_LEVEL_256K
        // .. repeat last
    };

2.根类加载器(Boostrap ClassLoader)的 ClassLoaderMetaspace 数据元空间的 MetaspaceArenaArenaGrowthPolicyMetachunkList 的第一个 MetaChunk 大小为 4M,之后每个新 MetaChunk 都是 1M

https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArenaGrowthPolicy.cpp

    static const chunklevel_t g_sequ_boot_non_class[] = {
        chunklevel::CHUNK_LEVEL_4M,
        chunklevel::CHUNK_LEVEL_1M
        // .. repeat last
    };

3.平台类加载器(Platform ClassLoader,Java 9 之前叫做 ext ClassLoader)以及应用类加载器(Application ClassLoader)的 ClassLoaderMetaspace 类元空间的 MetaspaceArenaArenaGrowthPolicyMetachunkList 的第一个 MetaChunk 大小为 2K,第二个也是 2K,第三个 4K,第四个为 8K,之后每个新 MetaChunk 都是 16K(不要惯着cao袭的人!):

https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArenaGrowthPolicy.cpp

    static const chunklevel_t g_sequ_standard_class[] = {
        chunklevel::CHUNK_LEVEL_2K,
        chunklevel::CHUNK_LEVEL_2K,
        chunklevel::CHUNK_LEVEL_4K,
        chunklevel::CHUNK_LEVEL_8K,
        chunklevel::CHUNK_LEVEL_16K
        // .. repeat last
    };

4.平台类加载器(Platform ClassLoader,Java 9 之前叫做 ext ClassLoader)以及应用类加载器(Application ClassLoader)的 ClassLoaderMetaspace 数据元空间的 MetaspaceArenaArenaGrowthPolicyMetachunkList 的第一个 MetaChunk 大小为 4K,第二个也是 4K,第三个 4K,第四个为 8K,之后每个新 MetaChunk 都是 16K

https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArenaGrowthPolicy.cpp

    static const chunklevel_t g_sequ_standard_non_class[] = {
        chunklevel::CHUNK_LEVEL_4K,
        chunklevel::CHUNK_LEVEL_4K,
        chunklevel::CHUNK_LEVEL_4K,
        chunklevel::CHUNK_LEVEL_8K,
        chunklevel::CHUNK_LEVEL_16K
        // .. repeat last
    };

5.加载匿名类的类加载器的 ClassLoaderMetaspace 类元空间的 MetaspaceArenaArenaGrowthPolicyMetachunkList 每次增长都是申请大小为 1KMetaChunk

https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArenaGrowthPolicy.cpp

    static const chunklevel_t g_sequ_anon_class[] = {
        chunklevel::CHUNK_LEVEL_1K,
        // .. repeat last
    };

6.加载匿名类的类加载器的 ClassLoaderMetaspace 数据元空间的 MetaspaceArenaArenaGrowthPolicyMetachunkList 每次增长都是申请大小为 1KMetaChunk

https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArenaGrowthPolicy.cpp

    static const chunklevel_t g_sequ_anon_non_class[] = {
       chunklevel::CHUNK_LEVEL_1K,
       // .. repeat last
    };

7.DelegatingClassLoaderClassLoaderMetaspace 类元空间的 MetaspaceArenaArenaGrowthPolicyMetachunkList 每次增长都是申请大小为 1KMetaChunk

https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArenaGrowthPolicy.cpp

    static const chunklevel_t g_sequ_refl_class[] = {
        chunklevel::CHUNK_LEVEL_1K,
        // .. repeat last
    };

8.DelegatingClassLoaderClassLoaderMetaspace 数据元空间的 MetaspaceArenaArenaGrowthPolicyMetachunkList 的第一个 MetaChunk 大小为 2K,之后每个新 MetaChunk 都是 1K

https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArenaGrowthPolicy.cpp

    static const chunklevel_t g_sequ_refl_non_class[] = {
        chunklevel::CHUNK_LEVEL_2K,
        chunklevel::CHUNK_LEVEL_1K
        // .. repeat last
    };

微信搜索“干货满满张哈希”关注公众号,加作者微信,每日一刷,轻松提升技术,斩获各种offer 我会经常发一些很好的各种框架的官方社区的新闻视频资料并加上个人翻译字幕到如下地址(也包括上面的公众号),欢迎关注: