全网最硬核 JVM 内存解析 - 10.元空间分配与回收流程举例

3,973 阅读16分钟

个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判。如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 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.4. 元空间分配与回收流程举例

我们前面介绍了元空间的组成元素,但是没有将他们完整的串联起来,我们这里举一个简单的例子,将之前的所有元素串联起来。

通过前面的分析之后,我们知道元空间的主要抽象包括:

  • 全局唯一的类元空间 MetaspaceContext,它包括:
    • 一个 VirtualSpaceList,类元空间的 VirtualSpaceList 只有一个 VirtualSpaceNode
    • 一个 ChunkManager
  • 全局唯一的数据元空间 MetaspaceContext,它包括:
    • 一个 VirtualSpaceList,数据元空间的 VirtualSpaceList 才是一个真正的 VirtualSpaceNode 的链表
    • 一个 ChunkManager
  • 每个类加载器都有一个独有的 ClassLoaderData,它包含自己独有的 ClassLoaderMetaspaceClassLoaderMetaspace 包含:
    • 一个类元空间 MetaspaceArena
    • 一个数据元空间 MetaspaceArena

假设我们全局只有一个类加载器,即类加载器 1,并且 UseCompressedClassPointerstrue,那么我们可以假设当前元空间的初始结构为:

image

接下来我们来看看详细的例子

4.4.1. 首先类加载器 1 需要分配 1023 字节大小的内存,属于类空间

image

1~2.首先,类加载器 1 从它私有的 ClassLoaderData 去分配空间,由于要分配的是类元空间的,所以会从私有的类元空间的 MetaspaceArena 去分配空间。

3.搜索 FreeBlocks 查看是否有可用空间,但是这是第一次分配,肯定没有。

4.尝试从 _current_chunk 分配,但是由于是第一次分配,_current_chunkNULL

image

5.将要分配的内存(1023 字节)按照 8 字节对齐,即 1024 字节。大于等于它的最小 ChunkLevel 为 12,即 max_level = 12。假设这个类加载器是 Bootstrap ClassLoader,其实是啥无所谓,我们主要是想找一个对应的 ArenaGrowthPolicy,根据这个 ArenaGrowthPolicy,第一个要申请的 MeataChunk 大小是 256KB,对应的 ChunkLevel 为 4,preferred_levelmax_level 与这个之间相比小的那个,即 4。我们从类元空间的 ChunkManager 申请这么大的 MetaChunk,对应的 ChunkLevel 是 4

6.首先搜索 ChunkManagerFreeChunkListVector,看看是否有合适的。但是这是第一次分配,肯定没有。

7.尝试从类元空间的 VirtualSpaceList 申请 RootMetaChunk 用于分配。

image

8.从类元空间的 VirtualSpaceList 的唯一一个 VirtualSpaceNode 分配 RootMetaChunk,对半切分到 ChunkLevel 为 4 的 MetaChunk,返回 leaderChunkLevel 为 4 的 MetaChunk 作为 _current_chunk 用于分配。分割出来剩下的 ChunkLevel 为 1, ChunkLevel 为 2, ChunkLevel 为 3, ChunkLevel 为 4 的各一个放入 FreeChunkListVector

9.commit 要分配的内存大小,如果 AlwaysPreTouch 是开启的,那么就会像之前我们分析 Java 堆内存那样进行 pre touch。

10.从 _current_chunk 分配内存,分配成功。

4.4.2. 然后类加载器 1 还需要分配 1023 字节大小的内存,属于类空间

image

1~2.首先,类加载器 1 从它私有的 ClassLoaderData 去分配空间,由于要分配的是类元空间的,所以会从私有的类元空间的 MetaspaceArena 去分配空间。

3.搜索 FreeBlocks 查看是否有可用空间,目前还是没有。

4.尝试从 _current_chunk 分配,将要分配的内存(1023 字节)按照 8 字节对齐,即 1024 字节,_current_chunk 空间足够。

5.commit 要分配的内存大小,如果 AlwaysPreTouch 是开启的,那么就会像之前我们分析 Java 堆内存那样进行 pre touch。

6.从 _current_chunk 分配内存,分配成功。

4.4.3. 然后类加载器 1 需要分配 264 KB 大小的内存,属于类空间

image

1~2.首先,类加载器 1 从它私有的 ClassLoaderData 去分配空间,由于要分配的是类元空间的,所以会从私有的类元空间的 MetaspaceArena 去分配空间。

3.搜索 FreeBlocks 查看是否有可用空间,目前还是没有。

4.尝试从 _current_chunk 分配,将要分配的内存(264KB)按照 8 字节对齐,即 264KB,_current_chunk 空间不足,但是如果扩容一倍就足够,所以尝试扩大 _current_chunk

image

5.查看他的兄弟 MetaChunk 是否是空闲的,当然是,从 FreeChunkListVector 移除这个 MetaChunk,将这个兄弟 MetaChunk_current_chunk_current_chunk 的大小变为原来 2 倍,_current_chunkChunkLevel 减 1 之后为 3。

6.commit 要分配的内存大小,如果 AlwaysPreTouch 是开启的,那么就会像之前我们分析 Java 堆内存那样进行 pre touch。

7.从 _current_chunk 分配内存,分配成功。

4.4.4. 然后类加载器 1 需要分配 2 MB 大小的内存,属于类空间

image

1~2.首先,类加载器 1 从它私有的 ClassLoaderData 去分配空间,由于要分配的是类元空间的,所以会从私有的类元空间的 MetaspaceArena 去分配空间。

3.搜索 FreeBlocks 查看是否有可用空间,目前还是没有。

4.尝试从 _current_chunk 分配,将要分配的内存(2MB)按照 8 字节对齐,即 2MB,_current_chunk 空间不足,扩容一倍也不够,所以就不尝试扩大 _current_chunk 了。

image

5.要分配的大小是 2MB,大于等于它的最小 ChunkLevel 为 1,即 max_level = 1。根据 ArenaGrowthPolicy,下一个要申请的 MeataChunk 大小是 256KB,对应的 ChunkLevel 为 4,preferred_levelmax_level 与这个之间相比小的那个,即 1。从 FreeChunkListVector 寻找,发现有合适的,将其作为 current_chunk 进行分配。

6.commit 要分配的内存大小,如果 AlwaysPreTouch 是开启的,那么就会像之前我们分析 Java 堆内存那样进行 pre touch。

7.之前的 current_chunk 的剩余空间大于 2 bytes,需要回收到 FreeBlocks 中。由于大于 33 bytes,需要放入 BlockTree

8.从 _current_chunk 分配内存,分配成功。

4.4.5. 然后类加载器 1 需要分配 128KB 大小的内存,属于类空间

image

1~2.首先,类加载器 1 从它私有的 ClassLoaderData 去分配空间,由于要分配的是类元空间的,所以会从私有的类元空间的 MetaspaceArena 去分配空间。

3.将要分配的内存(128KB)按照 8 字节对齐,即 128KB。搜索 FreeBlocks 查看是否有可用空间,目前 FreeBlocks 有合适的可以分配。

4.commit 要分配的内存大小,如果 AlwaysPreTouch 是开启的,那么就会像之前我们分析 Java 堆内存那样进行 pre touch。

5.从 FreeBlocksBlockTree 的节点分配内存,分配成功。为啥要打击抄袭,稿主被抄袭太多所以断更很久。

4.4.6. 新来一个类加载器 2,需要分配 1023 Bytes 大小的内存,属于类空间

image

1~2.首先,类加载器 1 从它私有的 ClassLoaderData 去分配空间,由于要分配的是类元空间的,所以会从私有的类元空间的 MetaspaceArena 去分配空间。

3.搜索 FreeBlocks 查看是否有可用空间,但是这是第一次分配,肯定没有。

4.尝试从 _current_chunk 分配,但是由于是第一次分配,_current_chunkNULL

image

5.将要分配的内存(1023 字节)按照 8 字节对齐,即 1024 字节。大于等于它的最小 ChunkLevel 为 12,即 max_level = 12。假设这个类加载器是 Bootstrap ClassLoader,其实是啥无所谓,我们主要是想找一个对应的 ArenaGrowthPolicy。根据 ArenaGrowthPolicy,下一个要申请的 MeataChunk 大小是 256KB,对应的 ChunkLevel 为 4,preferred_levelmax_level 与这个之间相比小的那个,即 4。

6.首先搜索 ChunkManagerFreeChunkListVector,看看是否有合适的。搜索到之前放入的 ChunkLevel 为 3 的。将其取出作为 _current_chunk

7.commit 要分配的内存大小,如果 AlwaysPreTouch 是开启的,那么就会像之前我们分析 Java 堆内存那样进行 pre touch。

8.从 _current_chunk 分配内存,分配成功。

4.4.7. 然后类加载器 1 被 GC 回收掉

image

1.将类加载器 1 消耗的所有空间放回 FreeBlocks 中。前面分配了 1024 bytes, 1024 bytes, 264KB, 2MB 还有 128KB,这次放回 BlockTreeBlockTree 之前本身还有剩余一个 118KB。整体如图所示。

2.这样一来,原来 MetaspaceArenaMetaChunkList 管理的 MetaChunk 的内存全都空闲了。

image

  1. MetaChunkList 管理的 MetaChunk 放回全局的 ChunkManagerFreeChunkListVector 中。并且放回的都是有 commit 过内存的,会放在每个 ChunkLevel 对应的 MetaChunk 链表的开头。

4.4.8. 然后类加载器 2 需要分配 1 MB 大小的内存,属于类空间

image

1~2.首先,类加载器 1 从它私有的 ClassLoaderData 去分配空间,由于要分配的是类元空间的,所以会从私有的类元空间的 MetaspaceArena 去分配空间。

3.搜索 FreeBlocks 查看是否有可用空间,目前还是没有。为啥要打击抄袭,稿主被抄袭太多所以断更很久。

4.尝试从 _current_chunk 分配,空间不足。并且 _current_chunk 不是 leader,所以就不尝试扩容了。

image

5.将要分配的内存(1MB)按照 8 字节对齐,即 1MB。要分配的大小是 1MB,大于等于它的最小 ChunkLevel 为 2,即 max_level = 2。根据 ArenaGrowthPolicy,下一个要申请的 MeataChunk 大小是 256KB,对应的 ChunkLevel 为 4,preferred_levelmax_level 与这个之间相比小的那个,即 2。从 FreeChunkListVector 寻找,发现有合适的,将其作为 current_chunk 进行分配。这个其实就是之前从类加载器 1 回收的。

6.因为是之前回收的,里面的内存都是 committed 了,所以这里就不用 commit 了。

7.之前的 current_chunk 的剩余空间大于 2 bytes,需要回收到 FreeBlocks 中。由于大于 33 bytes,需要放入 BlockTree

8.从 _current_chunk 分配内存,分配成功。

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