2024-01-25
原文作者:hashcon 原文地址: https://zhanghaoxin.blog.csdn.net/article/details/130377769

本篇是关于 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.6. jcmd VM.metaspace 元空间说明、元空间相关 JVM 日志以及元空间 JFR 事件详解

4.6.1. jcmd <pid> VM.metaspace 元空间说明

通过 jcmd <pid> VM.metaspace 命令可以查看对应 JVM 进程的元空间当前的详细使用情况,返回内容是:

1.元空间从 MetaChunk 角度的使用统计信息

    Total Usage - 1383 loaders, 33006 classes (1361 shared):
      Non-Class: 7964 chunks,    150.83 MB capacity,  150.77 MB (>99%) committed,   150.21 MB (>99%) used,   562.77 KB ( <1%) free,     6.65 KB ( <1%) waste , deallocated: 869 blocks with 249.52 KB
      Class: 2546 chunks,     21.00 MB capacity,   20.93 MB (>99%) committed,    20.21 MB ( 96%) used,   741.42 KB (  3%) free,   216 bytes ( <1%) waste , deallocated: 1057 blocks with 264.88 KB
      Both: 10510 chunks,   171.83 MB capacity,  171.70 MB (>99%) committed,   170.42 MB (>99%) used,     1.27 MB ( <1%) free,     6.86 KB ( <1%) waste , deallocated: 1926 blocks with 514.41 KB

意思是:

  1. 一共 1383 个类加载器,加载了 33006 个类(其中 1361 个是共享类)。
  2. capacity 是指 MetaChunk 的总容量大小(Reserved 内存);committed 是指这些 MetaChunkcommitted 的内存大小,也就是实际占用系统物理内存是这么大(虽然可能会有点细微差异,参考本篇文章的第二章);used 是指这些 MetaChunk 实际使用的大小,肯定比 committed 的要小;free 是指剩余的大小;committed = used + free + waste;deallocated 是指回收到 FreeBlocks 的大小,属于 free 的一部分,另一部分就是 MetaChunkcommitted 但是还没使用的部分;waste 是指浪费的大小(前面我们提到了什么造成的浪费,主要是搜索 FreeBlocks 的空间使用的时候,可能正好剩下 1 字节,就不放回了继续使用了)洗稿的狗也遇到不少
  3. 数据元空间使用情况:一共使用了 7964 个 MetaChunk,这些 MetaChunk 相关总容量大小是 150.83 MB,目前 commit150.77 MB,使用了 150.21 MB,剩余 562.77 KB 可以使用,6.65 KB 的空间被浪费了。FreeBlocks 目前回收了 869 块内存,一共 249.52 KB
  4. 类元空间使用情况:一共使用了 2546 个 MetaChunk,总容量大小是 21.00 MB,目前 commit20.93 MB,使用了 20.21 MB,剩余 741.42 KB 可以使用,216 bytes 的空间被浪费了。FreeBlocks 目前回收了 1057 块内存,一共 264.88 KB
  5. 总的元空间使用情况(类元空间 + 数据元空间的):一共使用了 10510 个 MetaChunk,总容量大小是 171.83 MB,目前 commit171.70 MB,使用了 170.42 MB,剩余 1.27 MB 可以使用,6.86 KB 的空间被浪费了。FreeBlocks 目前回收了 1926 块内存,一共 514.41 KB

前面的是从 MetaChunk 的角度去查看,另一个角度是从 VirtualSpaceList 去查看,接下来的信息就是:

    Virtual space:
      Non-class space:      152.00 MB reserved,     150.81 MB (>99%) committed,  19 nodes.
          Class space:        1.00 GB reserved,      20.94 MB (  2%) committed,  1 nodes.
                 Both:        1.15 GB reserved,     171.75 MB ( 15%) committed.

意思是:

  1. 数据元空间的 VirtualSpaceList:总共 Reserve152.00 MB,目前 Commit150.81 MB,一共有 19VirtualSpaceNode。这个与 MetaChunk 的统计信息是有差异的,VirtualSpaceList 的统计信息更体现元空间实际占用的,从 MetaChunk 角度统计的时候,将每个 MetaChunk 统计信息相加,会有精度损失。
  2. 类元空间的 VirtualSpaceList:总共 Reserve1.00 GB,目前 Commit20.94 MB,一共有 1VirtualSpaceNode
  3. 总的元空间的 VirtualSpaceList:总共 Reserve1.15 GB,目前 Commit171.75 MB。不要偷取他人的劳动成果,也不要浪费自己的时间和精力,让我们一起做一个有良知的写作者。

接下来是每个 ChunkManagerFreeChunkListVector 的统计信息:

    Chunk freelists:
       Non-Class:
    
      4m: (none)
      2m: (none)
      1m:    2, capacity=2.00 MB, committed=0 bytes (  0%)
    512k: (none)
    256k: (none)
    128k:    2, capacity=256.00 KB, committed=0 bytes (  0%)
     64k: (none)
     32k:    2, capacity=64.00 KB, committed=0 bytes (  0%)
     16k: (none)
      8k:    2, capacity=16.00 KB, committed=0 bytes (  0%)
      4k:    2, capacity=8.00 KB, committed=0 bytes (  0%)
      2k: (none)
      1k:    2, capacity=2.00 KB, committed=0 bytes (  0%)
    Total word size: 2.34 MB, committed: 0 bytes (  0%)
    
           Class:
    
      4m: (none)
      2m:    1, capacity=2.00 MB, committed=0 bytes (  0%)
      1m:    1, capacity=1.00 MB, committed=0 bytes (  0%)
    512k: (none)
    256k: (none)
    128k: (none)
     64k: (none)
     32k: (none)
     16k: (none)
      8k: (none)
      4k:    1, capacity=4.00 KB, committed=0 bytes (  0%)
      2k: (none)
      1k: (none)
    Total word size: 3.00 MB, committed: 0 bytes (  0%)
    
            Both:
    
      4m: (none)
      2m:    1, capacity=2.00 MB, committed=0 bytes (  0%)
      1m:    3, capacity=3.00 MB, committed=0 bytes (  0%)
    512k: (none)
    256k: (none)
    128k:    2, capacity=256.00 KB, committed=0 bytes (  0%)
     64k: (none)
     32k:    2, capacity=64.00 KB, committed=0 bytes (  0%)
     16k: (none)
      8k:    2, capacity=16.00 KB, committed=0 bytes (  0%)
      4k:    3, capacity=12.00 KB, committed=0 bytes (  0%)
      2k: (none)
      1k:    2, capacity=2.00 KB, committed=0 bytes (  0%)
    Total word size: 5.34 MB, committed: 0 bytes (  0%)

以上的信息可能用图片更直接一些:

202401252016037711.png

接下来是关于回收利用的从 MetaChunk 的角度去查看一些统计信息:

    Waste (unused committed space):(percentages refer to total committed size 171.75 MB):
            Waste in chunks in use:      6.86 KB ( <1%)
            Free in chunks in use:      1.27 MB ( <1%)
                    In free chunks:      0 bytes (  0%)
    Deallocated from chunks in use:    514.41 KB ( <1%) (1926 blocks)
                           -total-:      1.78 MB (  1%)
    
    chunk header pool: 10520 items, 748.30 KB.

包含的信息是:

  1. 当前被使用的 MetaChunk(即存在于每个类加载器对应的 MetaspaceArena 中的 MetaChunk)中有 6.86 KB 的空间被浪费了。当前被使用的 MetaChunk(即存在于每个类加载器对应的 MetaspaceArena 中的 MetaChunk)中剩余 1.27 MB 可以使用。在 FreeChunkListVector 中没有浪费的空间,其实从前面的 FreeChunkListVector 的详细信息就能看出来。
  2. FreeBlocks 目前回收了 1926 块内存,一共 514.41 KBFreeBlocks 里面有 1926FreeBlock,一共 514.41 KB
  3. ChunkHeaderPool 目前有 10520ChunkHeader,一共占用 748.30 KB

然后是一些统计信息:

    Internal statistics:
    
    num_allocs_failed_limit: 24.
    num_arena_births: 2768.
    num_arena_deaths: 2.
    num_vsnodes_births: 20.
    num_vsnodes_deaths: 0.
    num_space_committed: 2746.
    num_space_uncommitted: 0.
    num_chunks_returned_to_freelist: 28.
    num_chunks_taken_from_freelist: 10515.
    num_chunk_merges: 9.
    num_chunk_splits: 6610.
    num_chunks_enlarged: 4139.
    num_purges: 2.
    num_inconsistent_stats: 0.

包含的信息是:

  1. num_allocs_failed_limit:元空间普通分批内存失败的次数(前文分析过详细流程),后面也有对应的 JFR 事件会分析。
  2. num_arena_birthsMetaspaceArena 的创建次数。
  3. num_arena_deathsMetaspaceArena 的销毁次数。发生于对应的类加载器被回收之后。
  4. num_vsnodes_birthsVirtualSpaceNode 的创建次数。(根据前面的 VirtualSpaceList 的统计信息可以知道是 19 + 1 = 20)
  5. num_vsnodes_deathsVirtualSpaceNode 的销毁次数。
  6. num_space_committedCommit 内存的次数。
  7. num_space_uncommittedUncommit 内存的次数。
  8. num_chunks_returned_to_freelistMetaChunk 被回收到 FreeChunkListVector 的次数。
  9. num_chunks_taken_from_freelist:从 FreeChunkListVector 中获取 MetaChunk 进行分配的次数。
  10. num_chunk_mergesMetaChunk 合并的次数。
  11. num_chunk_splitsMetaChunk 拆分的次数。
  12. num_chunks_enlargedMetaChunk 扩容的次数。
  13. num_purgesMetaspaceArena 的清理次数。一般等于销毁次数。
  14. num_inconsistent_stats:不一致的统计次数。这个一般不用关心,主要是为了调试用的。

最后是一些参数信息:

    Settings:
    MaxMetaspaceSize: unlimited
    CompressedClassSpaceSize: 1.00 GB
    Initial GC threshold: 40.00 MB
    Current GC threshold: 210.12 MB
    CDS: on
    MetaspaceReclaimPolicy: balanced
     - commit_granule_bytes: 65536.
     - commit_granule_words: 8192.
     - virtual_space_node_default_size: 1048576.
     - enlarge_chunks_in_place: 1.
     - new_chunks_are_fully_committed: 0.
     - uncommit_free_chunks: 1.
     - use_allocation_guard: 0.
     - handle_deallocations: 1.
  1. MaxMetaspaceSize:元空间最大值。默认是无限制的。这里我们也没限制。
  2. CompressedClassSpaceSize:压缩类空间大小。默认是 1 GB。这里我们也没指定,所以是默认的。
  3. Initial GC threshold:初始的元空间 GC 阈值。默认是 40 MB。这里我们也没指定,所以是默认的。
  4. Current GC threshold:当前的元空间 GC 阈值。前面我们分析过这个阈值改变的机制。
  5. CDS:是否开启了 CDS。默认开启。这个我们不用太关心,主要和 CDS 特性相关(JEP 310: Application Class-Data SharingJEP 350: Dynamic CDS Archives),在以后的文章会详细分析。
  6. 元空间 MetaspaceReclaimPolicybalanced
  7. commit 粒度(commit_granule_bytes)为 65536 字节,转化单位为字之后,是 8192 字(一 word 为 8 字节)。虚拟内存空间节点内存大小(virtual_space_node_default_size)为 1048576 字,转化单位为字之后,是 64 MB。当前 MetaChunk 不足以分配的时候,是否尝试扩容当前 MetaChunkenlarge_chunks_in_place)为是,新分配的 MetaChunk 是否一次性全部 commit(new_chunks_are_fully_committed)为否,是否在 MetaChunk 释放的时候 uncommit(uncommit_free_chunks)为是。以上配置都在前文分析过。最后两个配置都是 debug 配置,正式版里面都是无法修改的,我们也不用太关心这两个配置的效果,并且 handle_deallocations 已经在 Java 18 中移除了(https://github.com/openjdk/jdk/commit/157e1d5073e221dab084422389f68eea53974f4c

4.6.2. 元空间相关 JVM 日志

我们通过启动参数 -Xlog:metaspace*=debug::utctime,level,tags,查看元空间相关 JVM 日志。

首先,初始化 JVM 元空间的时候,会输出元空间基本参数:

    [2023-04-11T09:07:31.994+0000][info][metaspace] Initialized with strategy: balanced reclaim.
    [2023-04-11T09:07:31.994+0000][info][metaspace]  - commit_granule_bytes: 65536.
    [2023-04-11T09:07:31.994+0000][info][metaspace]  - commit_granule_words: 8192.
    [2023-04-11T09:07:31.994+0000][info][metaspace]  - virtual_space_node_default_size: 1048576.
    [2023-04-11T09:07:31.994+0000][info][metaspace]  - enlarge_chunks_in_place: 1.
    [2023-04-11T09:07:31.994+0000][info][metaspace]  - new_chunks_are_fully_committed: 0.
    [2023-04-11T09:07:31.994+0000][info][metaspace]  - uncommit_free_chunks: 1.
    [2023-04-11T09:07:31.994+0000][info][metaspace]  - use_allocation_guard: 0.
    [2023-04-11T09:07:31.994+0000][info][metaspace]  - handle_deallocations: 1.

以上这几行日志的意思是:元空间 MetaspaceReclaimPolicybalanced,commit 粒度(commit_granule_bytes)为 65536 字节,转化单位为字之后,是 8192 字(一 word 为 8 字节)。虚拟内存空间节点内存大小(virtual_space_node_default_size)为 1048576 字,转化单位为字之后,是 64 MB。当前 MetaChunk 不足以分配的时候,是否尝试扩容当前 MetaChunkenlarge_chunks_in_place)为是,新分配的 MetaChunk 是否一次性全部 commit(new_chunks_are_fully_committed)为否,是否在 MetaChunk 释放的时候 uncommit(uncommit_free_chunks)为是。以上配置都在前文分析过。最后两个配置都是 debug 配置,正式版里面都是无法修改的,我们也不用太关心这两个配置的效果,并且 handle_deallocations 已经在 Java 18 中移除了(https://github.com/openjdk/jdk/commit/157e1d5073e221dab084422389f68eea53974f4c

接下来,初始化元空间的内存空间:

    [2023-04-11T09:07:32.411+0000][info ][gc,metaspace] CDS archive(s) mapped at: [0x0000000800000000-0x0000000800bde000-0x0000000800bde000), size 12443648, SharedBaseAddress: 0x0000000800000000, ArchiveRelocationMode: 0.
    [2023-04-11T09:07:32.411+0000][info ][gc,metaspace] Compressed class space mapped at: 0x0000000800c00000-0x0000000840c00000, reserved size: 1073741824
    [2023-04-11T09:07:32.411+0000][info ][gc,metaspace] Narrow klass base: 0x0000000800000000, Narrow klass shift: 0, Narrow klass range: 0x100000000
    [2023-04-11T09:07:32.417+0000][debug][metaspace   ] Arena @0x0000ffff807a1cc0 (non-class sm): : born.
    [2023-04-11T09:07:32.417+0000][debug][metaspace   ] Arena @0x0000ffff807a1dd0 (class sm): : born.
    [2023-04-11T09:07:32.417+0000][debug][metaspace   ] CLMS @0x0000ffff807a1c80 : born (nonclass arena: 0x0000ffff807a1cc0, class arena: 0x0000ffff807a1dd0.
    [2023-04-11T09:07:32.411+0000][debug][metaspace   ] VsListNode @0x0000ffff80784ab0 base 0x0000000800c00000 : born (word_size 134217728).
    [2023-04-11T09:07:32.417+0000][debug][metaspace   ] VsListNode @0x0000ffff807a27b0 base 0x0000ffff52800000 : born (word_size 1048576).

这几行日志的意思是:

  1. CDS 元数据映射到内存的地址范围是 [0x0000000800000000-0x0000000800bde000-0x0000000800bde000),大小为 12443648 字节,共享基地址为 0x0000000800000000ArchiveRelocationMode 为关闭。这些信息我们不用太关心,主要和 CDS 特性相关(JEP 310: Application Class-Data SharingJEP 350: Dynamic CDS Archives),在以后的文章会详细分析。
  2. 我们这里是默认配置,所以压缩类空间是开启的,初始化压缩类空间,映射到内存的地址范围是 [0x0000000800c00000-0x0000000840c00000),Reserved 内存大小为 1073741824 字节(1GB),默认压缩类空间最大大小就是 1GB。加载到压缩类空间的类的基地址为 0x0000000800000000(),偏移量为 0,范围为 0x100000000,这个前面也简单分析过。
  3. Bootstrap ClassLoader 创建了两个 MetaspaceArena,分别是前文分析的类元空间的 MetaspaceArena 和数据元空间的 MetaspaceArena,放入对应的 ClassLoadMetaSpace 中。不要偷取他人的劳动成果,也不要浪费自己的时间和精力,让我们一起做一个有良知的写作者。
  4. 初始化类元空间的还有数据元空间的 VirtualSpaceList,并分别创建并放入各自的第一个 VirtualSpaceNode

接下来开始加载类,从元空间申请内存进行分配:

    [2023-04-11T09:07:32.411+0000][debug][metaspace] ChkMgr @0x0000ffff807863d0 (class-space): requested chunk: pref_level: lv12, max_level: lv12, min committed size: 0.
    [2023-04-11T09:07:32.411+0000][debug][metaspace] VsListNode @0x0000ffff80784ab0 base 0x0000000800c00000 : new root chunk @0x0000ffff807867f0, f, base 0x0000000800c00000, level lv00.
    [2023-04-11T09:07:32.411+0000][debug][metaspace] ChkMgr @0x0000ffff807863d0 (class-space): allocated new root chunk.
    [2023-04-11T09:07:32.411+0000][debug][metaspace] ChkMgr @0x0000ffff807863d0 (class-space): splitting chunk @0x0000ffff807867f0, f, base 0x0000000800c00000, level lv00 to lv12.
    [2023-04-11T09:07:32.411+0000][debug][metaspace] ChkMgr @0x0000ffff807863d0 (class-space): handing out chunk @0x0000ffff807867f0, u, base 0x0000000800c00000, level lv12.

这几行日志的意思分别是:

  1. 加载类需要从元空间申请内存,这是第一次申请,所以各个数据结构都是空的,所以需要申请新的 MetaChunk,优先考虑的与最大的 ChunkLevel 都是 12,对应 1KB。本次申请发生在 ChunkManager @0x0000ffff807863d0
  2. 申请新的 RootMetaChunk,基址 0x0000000800c00000
  3. 将新的 RootMetaChunk 按照之前的算法拆分到 ChunkLevel12,结果是 MetaChunk @0x0000ffff807867f0,将拆出来的其他 MetaChunk 放入 ChunkManager @0x0000ffff807863d0FreeListVector

4.6.3. 元空间 JFR 事件详解

4.6.3.1. jdk.MetaspaceSummary 元空间定时统计事件

元空间定时统计事件 jdk.MetaspaceSummary,包括以下属性:

  • 事件开始时间:其实就是事件发生时间
  • GC Identifier:全局 GC 的 id 标识
  • When:事件发生的时机,包括 Before GCAfter GC 两种,分别是 GC 前和 GC 后的统计数据,可以根据 GC Identifier 对比 GC 前后的数据,看看 GC 之后元空间的使用情况.plagiarism和洗稿是恶意抄袭他人劳动成果的行为,是对劳动价值的漠视和践踏!
  • GC Threshold:GC 阈值,即前面提的 _capacity_until_GC
  • Class:Reserved:类元空间 Reserved 的内存空间大小
  • Class:Committed:类元空间 Committed 的内存空间大小
  • Class:Used:类元空间实际保存数据使用的内存空间大小(前面的机制分析中我们会看到,Committed 的空间会比实际使用的大,主要因为类加载器回收,以及可能 MetaChunk 分配的时候 commit 所有内存)
  • Data:Reserved:数据元空间 Reserved 的内存空间大小
  • Data:Committed:数据元空间 Committed 的内存空间大小
  • Data:Used:数据元空间实际保存数据使用的内存空间大小
  • Total:Reserved:整个元空间 Reserved 的内存空间大小(其实就是类元空间 + 数据元空间)
  • Total:Committed:整个元空间 Committed 的内存空间大小(其实就是类元空间 + 数据元空间)
  • Total:Used:整个元空间实际保存数据使用的内存空间大小(其实就是类元空间 + 数据元空间)

202401252016041612.png

4.6.3.2. jdk.MetaspaceAllocationFailure 元空间分配失败事件

前面提到过,如果普通分配失败,那么会触发 jdk.MetaspaceAllocationFailure 这个 JFR 事件,大家可以监控这个事件,去调整元空间大小减少由于元空间不足触发的 GC,这个事件包括以下属性:

  • 事件开始时间:其实就是事件发生时间
  • 类加载器:触发 OOM 的类加载器
  • Hidden Class Loader:是否是隐藏类加载器
  • Metadata Type:元数据类型,分为属于类元空间的以及属于数据元空间的两种类型,分别是:ClassMetadata
  • Metaspace Object Type:元空间对象类型,包括 ClassConstantPoolSymbolMethodKlassModulePackageOther
  • Size:本次分配的大小

这个事件也会采集堆栈信息,用来定位分配失败的源头是哪些类的加载导致的。

202401252016047983.png

4.6.3.3. jdk.MetaspaceOOM 元空间 OOM 事件

前面提到过,当元空间 OOM 的时候,就会产生这个事件,这个事件包括以下属性(和 jdk.MetaspaceAllocationFailure 事件一样):

  • 事件开始时间:其实就是事件发生时间
  • 类加载器:触发 OOM 的类加载器
  • Hidden Class Loader:是否是隐藏类加载器
  • Metadata Type:元数据类型,分为属于类元空间的以及属于数据元空间的两种类型,分别是:ClassMetadata
  • Metaspace Object Type:元空间对象类型,包括 ClassConstantPoolSymbolMethodKlassModulePackageOther
  • Size:本次分配的大小

jdk.MetaspaceAllocationFailure 事件一样,也会采集堆栈信息,用来定位 OOM 的原因。

202401252016052794.png

4.6.3.4. jdk.MetaspaceGCThreshold 元空间 GC 阈值变化事件

前面我们说过,元空间的 GC 阈值(_capacity_until_GC)是动态调整的,这个事件就是用来记录元空间 GC 阈值变化的。这个事件包括以下属性:

  • 事件开始时间:其实就是事件发生时间

  • New Value:新的 GC 阈值

  • Old Value:旧的 GC 阈值

  • Updater:哪个机制触发的 GC 阈值修改,我们之前讨论过 _capacity_until_GC 有两个场景会修改:

    • 分配过程中,达到 GC 阈值,触发 GC,但是处于 GCLocker 处于锁定禁止 GC,就尝试增大 _capacity_until_GC 进行分配。对应的 Updaterexpand_and_allocate
    • 每次 GC 之后,触发重新计算 _capacity_until_GC,如果有更新,就会生成这个事件,对应的 Updatercompute_new_size

202401252016057655.png

4.6.3.5. jdk.MetaspaceChunkFreeListSummary 元空间 Chunk FreeList 统计事件

这个事件在 Java 16 引入 JEP 387: Elastic Metaspace 弹性元空间的设计之后,里面的统计数据就都是 0 了,还没有实现,参考:https://bugs.openjdk.org/browse/JDK-8251342,所以我们先不用关心。参考源码:https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/share/memory/metaspaceUtils.hpp

    // (See JDK-8251342). Implement or Consolidate.
    static MetaspaceChunkFreeListSummary chunk_free_list_summary(Metaspace::MetadataType mdtype) {
        return MetaspaceChunkFreeListSummary(0,0,0,0,0,0,0,0);
    }

202401252016064626.png

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

  • 知乎:https://www.zhihu.com/people/zhxhash
  • B 站:https://space.bilibili.com/31359187
阅读全文