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

2023-05-01 16:23:57 浏览数 (1)

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

  • 全网最硬核 TLAB 解析
  • 全网最硬核 Java 随机数解析
  • 全网最硬核 Java 新内存模型解析

本篇是关于 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.5. 元空间大小限制与动态伸缩

前文我们没有提到,如何限制元空间的大小,其实就是限制 commit 的内存大小。元空间的限制不只是受限于我们的参数配置,并且前面我们提到了,元空间的内存回收也比较特殊,元空间的内存基本都是每个类加载器的 ClassLoaderData 申请并管理的,在类加载器被 GC 回收后,ClassLoaderData 管理的这些元空间也会被回收掉。所以,GC 是可能触发一部分元空间被回收了。所以元空间在设计的时候,还有一个动态限制 _capacity_until_GC,即触发 GC 的元空间占用大小。当要分配的空间导致元空间整体占用超过这个限制的时候,尝试触发 GC。这个动态限制也会在每次 GC 的时候动态扩大或者缩小。动态扩大以及缩小

我们先回顾下之前提过的参数配置:

  • MetaspaceSize:初始元空间大小,也是最小元空间大小。后面元空间大小伸缩的时候,不会小于这个大小。默认是 21M。
  • MaxMetaspaceSize:最大元空间大小,默认是无符号 int 最大值。
  • MinMetaspaceExpansion:每次元空间大小伸缩的时候,至少改变的大小。默认是 256K。
  • MaxMetaspaceExpansion:每次元空间大小伸缩的时候,最多改变的大小。默认是 4M。
  • MaxMetaspaceFreeRatio:最大元空间空闲比例,默认是 70,即 70%。
  • MinMetaspaceFreeRatio:最小元空间空闲比例,默认是 40,即 40%。

4.5.1. CommitLimiter 的限制元空间可以 commit 的内存大小以及限制元空间占用达到多少就开始尝试 GC

CommitLimiter 是一个全局单例,用来限制元空间可以 commit 的内存大小。每次分配元空间 commit 内存的时候,都会调用 CommitLimiter::possible_expansion_words 方法,这个方法会检查:

  1. 当前元空间已经 commit 的内存大小加上要分配的大小是否超过了 MaxMetaspaceSize
  2. 当前元空间已经 commit 的内存大小加上要分配的大小是否超过了 _capacity_until_GC,超过了就尝试触发 GC

尝试 GC 的核心逻辑是:

  1. 重新尝试分配
  2. 如果还是分配失败,检查 GCLocker 是否锁定禁止 GC,如果是的话,首先尝试提高 _capacity_until_GC 进行分配,分配成功直接返回,否则需要阻塞等待 GCLocker 释放
  3. 如果没有锁定,尝试触发 GC,之后回到第 1 步 (这里有个小参数 QueuedAllocationWarningCount,如果尝试触发 GC 的次数超过这个次数,就会打印一条警告日志,当然 QueuedAllocationWarningCount 默认是 0,不会打印,并且触发多次 GC 也无法满足的概率比较低)

4.5.2. 每次 GC 之后,也会尝试重新计算 _capacity_until_GC

在 JVM 初始化的时候,_capacity_until_GC 先会设置为 MaxMetaspaceSize,因为 JVM 初始化的时候会加载很多类,并且这时候要避免触发 GC。在初始化之后,将 _capacity_until_GC 设置为当前元空间占用大小与 MetaspaceSize 中比较大的那个值。同时,还会初始化一个 _shrink_factor,这个 _shrink_factor 主要是如果需要缩小元空间大小,每次缩小的比例。洗稿的狗也遇到不少

之后,在每次 GC 回收之后,需要重新计算新的 _capacity_until_GC

  1. 读取 crrent_shrink_factor = _shrink_factor,统计当前元空间使用的空间 used_after_gc
  2. 首先看是否需要扩容:
    1. 先使用 MinMetaspaceFreeRatio 最小元空间空闲比例计算 minimum_free_percentagemaximum_used_percentage,看是否需要扩容。
    2. 计算当前元空间至少要多大 minimum_desired_capacity:使用当前元空间使用的空间 used_after_gc 除以 maximum_used_percentage,并且保证它不小于初始元空间大小 MetaspaceSize,不大于最大元空间大小 MaxMetaspaceSize
    3. 如果当前的 _capacity_until_GC 小于计算的当前元空间至少要多大 minimum_desired_capacity,那么就查要扩容的空间是否大于等于配置 MinMetaspaceExpansion,以及小于等于 MaxMetaspaceExpansion,只有满足才会真正扩容。
    4. 扩容其实就是增加 _capacity_until_GC
  3. 然后看是否需要缩容:
    1. 使用 MaxMetaspaceFreeRatio 最大元空间空闲比例计算 minimum_free_percentagemaximum_used_percentage,看是否需要缩容。
    2. 计算当前元空间至少要多大 maximum_desired_capacity:使用当前元空间使用的空间 used_after_gc 除以 maximum_used_percentage,并且保证它不小于初始元空间大小 MetaspaceSize,不大于最大元空间大小 MaxMetaspaceSize
    3. 如果当前的 _capacity_until_GC 大于计算的当前元空间至少要多大 maximum_desired_capacity,计算 shrink_bytes = _capacity_until_GC 减去 maximum_desired_capacity
    4. _shrink_factor 初始为 0,之后为 10%,之后每次翻 4 倍,直到 100%。扩容的大小为 shrink_bytes 乘以这个百分比
    5. 如果缩容大于等于配置 MinMetaspaceExpansion,以及小于等于 MaxMetaspaceExpansion,并且缩容后不会小于初始元空间大小 MetaspaceSize,就会缩容。
    6. 缩容其实就是减少 _capacity_until_GC

我们还可以看出,如果我们设置 MinMetaspaceFreeRatio 为 0,那么就不会扩容,如果设置 MaxMetaspaceFreeRatio 为 100,那么就不会缩容。_capacity_until_GC 就不会因为 GC 更改。

0 人点赞