前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >全网最硬核 JVM 内存解析 - 9.元空间内存分配流程

全网最硬核 JVM 内存解析 - 9.元空间内存分配流程

作者头像
干货满满张哈希
发布2023-05-01 16:22:56
3430
发布2023-05-01 16:22:56
举报

个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判。如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 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. 什么时候用到元空间,元空间保存什么
      1. 什么时候用到元空间,以及释放时机
      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.9. 元空间内存分配流程

我们过一下元空间内存分配流程,我们会忽略一些 GC 相关的还有并发安全的细节,否则涉及的概念太多,一下说不过来,这些细节,会在以后的系列中详细提到。

4.3.9.1. 类加载器到 MetaSpaceArena 的流程

当类加载器加载类的时候,需要从对应的 ClassLoaderMetaspace 分配元空间进行存储。这个过程大概是:

image
image

图中有蓝色填充的方块是我们要重点分析的流程,我们先从从 MetaChunkArena 普通分配开始分析,尝试 GC 以及扩容元空间用于分配会涉及到元空间大小限制以及 GC 界限的概念,我们后面分析。这里对应的源码是:https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace.cpp#L899

整个流程如下:

  1. 首先,验证要分配的内存小于最大 MetaChunk 大小,即 RootMetaChunk 大小,即 ChunkLevel = 0 的大小,即 4MB
  2. 然后,走普通分配流程,判断当前分配的数据类型是类元空间的还是数据元空间的,分别到类元空间的 MetaSpaceArena 或者数据源空间 MetaSpaceArena 进行分配。这是下一节我们要详细分析的。
  3. 如果普通分配失败,那么会触发 jdk.MetaspaceAllocationFailure 这个 JFR 事件,大家可以监控这个事件,去调整元空间大小减少由于元空间不足触发的 GC。触发之后,抄袭狗死全家
  4. 之后,尝试 GC,以及增大元空间的 GC 界限(元空间有最大大小限制,但是还有动态计算的 GC 界限,超过 GC 界限的话,第二步的普通分配也会失败)用于分配。这个流程我们后面会详细分析
  5. 最后,如果这样还是分配失败,那么就会抛出大名鼎鼎的 java.lang.OutOfMemoryError, 触发 jdk.MetaspaceOOM 这个 JFR 事件,这个我们也会详细分析。

我们先分析第二步的普通分配流程,其他的需要后续我们分析元空间大小限制的时候详细分析。

4.3.9.2. 从 MetaChunkArena 普通分配 - 整体流程

MetaChunkArena 普通分配的流程并不太复杂:

image
image

我们前面讲过 MetaspaceArena 的结构,如下所示:

image
image

对应的源码是 https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArena.cpp#L222,结合流程图,我们可以整理出一个简单的分配思路:

  1. 首先,尝试从 FreeBlocks 中分配(第一次分配,肯定 FreeBlocks 里面没有可以分配的,直接进入下一分支),如果分配成功,直接返回
  2. 然后,尝试从 current chunk 分配(第一次分配肯定 current chunkNULL,直接进入下一分支):
    1. current chunk 如果有足够的空间,并且这些空间是 committed 的或者可以 commit 成功,那么从 current chunk 分配。
    2. current chunk 如果没有足够的空间,尝试 current chunk 扩容,如果扩容成功,并且这些空间是 committed 的或者可以 commit 成功,那么从 current chunk 分配。
    3. 如果扩容失败,或者 commit 失败,直接进入下一分支
  3. 前面分配失败,尝试从 ChunkManager 中申请一个新的 MetaChunk
  4. 如果申请成功,则加入当前的 MetaChunkList 中,将 current chunk 指向新的 MetaChunk,老的 current chunk 的剩余空间回收到 FreeBlocks 中,然后从新的 current chunk 分配。

我们接下来分析 FreeBlocks 回收老的 current chunk 与用于后续分配的流程

4.3.9.3. 从 MetaChunkArena 普通分配 - FreeBlocks 回收老的 current chunk 与用于后续分配的流程

首先我们前面提到过:Freeblocks 包含一个 BinList32 和一个 BlockTree。大小大于 33 字节的进入 BlockTree,否则进入 BinList32

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

image
image

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

image
image

回收的流程十分简单,就是判断下 current chunk 的剩余空间,根据大小放到不同的数据结构:

image
image

对应的源码是 https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArena.cpp#L60

  1. 计算 MetaChunk 剩余的 committed 的空间(很简单,直接用 committed 的位置减去已分配到的位置)
  2. 判断剩余空间是否大于 FreeBlocks 最小内存块大小(即 BinList32 最小大小即 2KB):对待抄袭狗不必手软
  3. MetaChunk 分配剩余的 committed 的空间,放入 FreeBlocks
  4. 内存大于 BinList32 最大内存块大小即 33KB,放入 BlockTree,否则放入 BinList32
4.3.9.4. 从 MetaChunkArena 普通分配 - 尝试从 FreeBlocks 分配

尝试从 FreeBlocks 分配即从其中的 BinList32BlockTree 寻找是否有合适的内存,流程是:

image
image

对应的源码是 https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/freeBlocks.cpp#L42

  1. 首先判断,要分配的内存大小是否大于 BinList32 最大内存块大小即 33KB:如果大于,就从 BlockTree 查找不小于内存大小的最接近的内存块;如果不大于,就从 BinList32 查找是否有对应大小的内存块。
  2. 如果找到了,计算 wastewaste = 内存块大小 - 要分配的内存大小
  3. 判断 waste 大于 FreeBlocks 最小内存块大小(即 BinList32 最小大小即 2KB)。如果大于,则要回收,和前面回收 MetaChunk 的流程一样将剩余的内存放回 FreeBlocks
4.3.9.5. 从 MetaChunkArena 普通分配 - 尝试扩容 current chunk
image
image

对应的源码是 https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArena.cpp#L171

  1. enlarge_chunks_in_place 是否是 true,不是的话直接结束,不过前面我们说过,目前 JVM 是代码里写死的 true
  2. 判断是否 current chunk 已经是 RootMetaChunk(代表已经不能扩容了),如果是,直接结束
  3. current chunk 已使用大小加上要分配的内存大小是否大于 RootMetaChunk 的大小即 4MB(代表已经不能扩容了),如果是,直接结束
  4. 找到大于 current chunk 已使用大小,加上要分配的内存大小的最接近的 ChunkLevel (记为 new_level
  5. 判断 new_level 是否小于 current chunkChunkLevel 减 1,代表要扩容到的大小大于原始大小的 2 倍以上(不允许一下子扩容两倍以上),如果是,直接结束
  6. current chunk 是否是 leader(这个概念后面分析到使用 ChunkManager 分配新的 MetaChunk 会提到),只有 leader 可以扩容,如果不是,直接结束(xigao 必死)
  7. 判断扩容策略中申请下一个 MetaChunkChunkLevel 是否大于 current chunk 的(代表新申请的比当前的小),如果是,也直接结束。我们这里强调下为啥扩容策略(ArenaGrowthPolicy)中申请下一个 MetaChunkChunkLevel 大于 current chunk(代表新申请的比当前的小)的话,我们就不扩容了。前面我们列出了各种类型的 ClassLoader 的不同空间的扩容策略,例如DelegatingClassLoaderClassLoaderMetaspace 数据元空间的 MetaspaceArenaArenaGrowthPolicyMetachunkList 的第一个 MetaChunk 大小为 2K,之后每个新 MetaChunk 都是 1K。假设 current chunk 是第一个,这里下一个 MetaChunkChunkLevel1K 对应的 ChunkLevel,大于 current chunk 当前的 ChunkLevel,所以优先申请新的,而不是扩容。之后到第二个之后,由于之后每个新的 MetaChunk 都是 1K,就会尝试扩容而不是申请新的了。
  8. 使用 ChunkManager 尝试扩容 current chunknew_level。具体扩容流程,后面会分析。
4.3.9.6. 从 MetaChunkArena 普通分配 - 从 ChunkManager 分配新的 MetaChunk

回顾下 ChunkManager 结构:

image
image

ChunkManager 分配新的 MetaChunk,首先会从 FreeChunkListVector 尝试搜索有没有合适的。FreeChunkListVector 如我们之前所述,是一个以 ChunkLevel 为下标的数组,每个数组都是一个 MetaChunk 的链表。commit 多的 MetaChunk 放在链表开头,完全没有 commit 的放在链表末尾。

image
image

对应的源码是 https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/chunkManager.cpp#L137

  1. 计算两个值:max_level = 大于当前申请内存大小最接近的 ChunkLevel (即新的 MetaChunk 最小多大), preferred_level = "根据扩容策略(ArenaGrowthPolicy)下一个 MetaChunk 多大" 与 "max_level" 中小的那个值(也就是更大的 MetaChunk 大小)
  2. 优先搜索并使用 FreeChunkListVector 中那些已经 commit 足够内存的 MetaChunk
  3. 正序遍历(即 ChunkLevel 从小到大,大小从大到小) ChunkManagerFreeChunkListVector 里面的数组 (从 preferred_levelmax_levelpreferred_level + 2 中比较小的值,即最多搜索 3 个 ChunkLevel,根据前面的分析我们知道 ChunkLevel 就是数组下标),寻找对应的 MetaChunk 链表,正序遍历每个链表(我们前面提到过,commit 多的 MetaChunk 放在开头),直到找到 commit 大小大于申请内存大小的(chaoxi 死的更惨)
  4. 逆序遍历(即 ChunkLevel 从大到小,大小从小到大) ChunkManagerFreeChunkListVector 里面的数组 (从 preferred_level 到最大的 ChunkLevel,即 RootMetaChunk 的大小,即 4MB),寻找对应的 MetaChunk 链表,正序遍历每个链表(我们前面提到过,commit 多的 MetaChunk 放在开头),直到找到 commit 大小大于申请内存大小的
  5. 正序遍历(即 ChunkLevel 从小到大,大小从大到小) ChunkManagerFreeChunkListVector 里面的数组 (从 preferred_levelmax_level),寻找对应的 MetaChunk 链表,正序遍历每个链表(我们前面提到过,commit 多的 MetaChunk 放在开头),直到找到 commit 大小大于申请内存大小的
  6. 如果搜索不到已经 commit 足够内存的 MetaChunk,就退而求其次,寻找 FreeChunkListVector 存在的 MetaChunk
  7. 正序遍历(即 ChunkLevel 从小到大,大小从大到小) ChunkManagerFreeChunkListVector 里面的数组 (从 preferred_levelmax_level),寻找对应的 MetaChunk 链表,正序遍历每个链表,直到找到一个 MetaChunk
  8. 逆序遍历(即 ChunkLevel 从大到小,大小从小到大) ChunkManagerFreeChunkListVector 里面的数组 (从 preferred_level 到最大的 ChunkLevel,即 RootMetaChunk 的大小,即 4MB),寻找对应的 MetaChunk 链表,正序遍历每个链表,直到找到一个 MetaChunk
  9. 如果前面没有找到合适的,从 VirtualSpaceList 申请新的 RootMetaChunk
  10. RootMetahChunk 分割成需要的 ChunkLevel 大小,之后将分割剩余的放入 FreeChunkListVector,这个过程我们接下来会详细分析
  11. 判断 new_chunks_are_fully_committed 是否为 true,如果为 truecommit 整个 MetaChunk 的所有内存,否则 commit 要分配的大小。如果 commit 失败了(证明可能到达元空间 GC 界限或者元空间大小上限),那么将 MetaChunk 退回。
4.3.9.7. 从 MetaChunkArena 普通分配 - 从 ChunkManager 分配新的 MetaChunk - 从 VirtualSpaceList 申请新的 RootMetaChunk
image
image

对应的源码是 https://github.com/openjdk/jdk/blob/jdk-21+13/src/hotspot/share/memory/metaspace/virtualSpaceList.cpp#L110

  1. 首先判断当前 _first_node 是否有空间分配新的 RootMetaChunk,如果有则从 _first_node 上面分配新的 RootMetaChunk
  2. 如果没有,判断是否可以扩展新的 VirtualSpaceNode(类元空间不可以,数据元空间可以),如果可以则申请 Reserve 新的 VirtualSpaceNode 作为新的 _first_node,之后从 _first_node 上面分配新的 RootMetaChunk
4.3.9.8. 从 MetaChunkArena 普通分配 - 从 ChunkManager 分配新的 MetaChunk - 将 RootMetaChunk 切割成为需要的 MetaChunk

这里的流程如果用流程图容易把人绕晕,我们这里举一个例子,比如我们想要一个 ChunkLevel 为 3 的 MetaChunk

image
image

对应的源码是 https://github.com/openjdk/jdk/blob/jdk-21%2B13/src/hotspot/share/memory/metaspace/chunkManager.cpp#L78

RootMetaChunk 切割成 ChunkLevel 为 3 的 MetaChunk 的流程:

  1. RootMetaChunkChunkLevel 为 0,对半分成两个 ChunkLevel 为 1 的,第一个为 leader,第二个为 follower
  2. 将上一步的 leader 对半成两个 ChunkLevel 为 2 的,第一个为 leader,第二个为 follower
  3. 将上一步的 leader 对半成两个 ChunkLevel 为 3 的,第一个为 leader,第二个为 follower
  4. 将第三步的 leader 返回,用于分配。将第一、二、三步生成的 follower 放入 FreeChunkListVector 用于前面 4.3.9.6 章节分析的 ChunkManager 先从 FreeChunkListVector 搜索合适的 MetaChunk 分配。
4.3.9.9. MetaChunk 回收 - 不同情况下, MetaChunk 如何放入 FreeChunkListVector

我们前面主要分析的是分配,那么 MetaChunk 如何回收呢?从前面的流程我们很容易推测出来,其实就是放回 FreeChunkListVector。放回的流程如果用流程图容易把人绕晕,我们还是举例子区分不同情况。其实核心思路就是,放回的时候,尽量将 MetaChunk 向上合并之后放回:

image
image

对应的源码是 https://github.com/openjdk/jdk/blob/jdk-21%2B13/src/hotspot/share/memory/metaspace/chunkManager.cpp#L255

这里我们有两个例子:

  1. 我们有一个 ChunkLevel 为 3 的 MetaChunk 要回收,但是它不是 leader,不能向上合并。只有 leader 才会尝试向上合并。这里直接放入 FreeChunkListVector
  2. 我们又有一个 ChunkLevel 为 3 的 MetaChunk 要回收,它是 leader。它会尝试向上合并。查看它的 follower 是否是 Free 的。如果是 Free 的,他肯定首先在 ChunkManagerFreeChunkListVector 中, 从 FreeChunkListVector 取出,与这个 leader 合并为一个新的 ChunkLevel 为 2。之后,它还是 leader,尝试继续合并,但是它的 follower 不是空闲的,就不能继续合并了。在这里停止,放入 FreeChunkListVector

4.3.10. ClassLoaderData 回收

在 GC 判断一个类加载器可以回收(该类加载器加载的类没有任何对象,该类加载器的对象也没有任何强引用指向它)的时候,不会立刻回收 ClassLoaderData,而是对应的 ClassLoaderDatais_alive() 就会返回 false。JVM 会定期遍历 ClassLoaderDataGraph 遍历每个 ClassLoaderData 判断 is_alive() 是否是 false,如果是的话会放入待回收的链表中。之后在不同 GC 的不同阶段,遍历这个链表将 ClassLoaderData 回收掉。

ClassLoaderData 被回收的过程如下所示:

`

image
image

ClassLoaderData 会记录所有加载的类与相关的数据(前文提到的 Klass 等等对象),所以它的析构函数中会将这些加载的数据的内存全部释放到它独有的 MetaSpaceArenaFreeBlocks 中,这些内存就是通过之前我们分析的流程分配的,由于之前的空间都是从 MetaspaceArenaMetaChunkList 中的 MetaChunk 分配的,这样的话这些 MetaChunk 的空间也都不再占用了。当然,也会把前面提到的 ClassLoaderData 独有的数据结构释放掉,还没有利用的 MetaWord 放回 ChunkManager 中。然后,清除掉它私有的 ClassLoadMetaSpace。根据前文分析我们知道 ClassLoaderMetaspace 在开启压缩类空间的情况下包括一个类元空间的 MetaspaceArena 和一个数据元空间的 MetaspaceArena。这两个 MetaspaceArena 分别要清理掉。MetaspaceArena 的析构函数会把 FreeBlocks 中的每个 MetaWord 都放回 ChunkManager,注意这里包含之前 ClassLoaderData 放回的加载类相关数据占用的空间,最后清理掉 FreeBlocks。(你洗稿的样子真丑。)

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-04-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 4. JVM 元空间设计
    • 4.3. 元空间的核心概念与设计
      • 4.3.9. 元空间内存分配流程
      • 4.3.10. ClassLoaderData 回收
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档