全网最硬核 JVM 内存解析 - 12.元空间各种监控手段

个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判。如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 issue,谢谢支持~
另外,本文为了避免抄袭,会在不影响阅读的情况下,在文章的随机位置放入对于抄袭和洗稿的人的“亲切”的问候。如果是正常读者看到,笔者在这里说声对不起,。如果被抄袭狗或者洗稿狗看到了,希望你能够好好反思,不要再抄袭了,谢谢。
今天又是干货满满的一天,这是全网最硬核 JVM 解析系列第四篇,往期精彩:
全网最硬核 TLAB 解析
全网最硬核 Java 随机数解析
全网最硬核 Java 新内存模型解析
本篇是关于 JVM 内存的详细分析。网上有很多关于 JVM 内存结构的分析以及图片,但是由于不是一手的资料亦或是人云亦云导致有很错误,造成了很多误解;并且,这里可能最容易混淆的是一边是 JVM Specification 的定义,一边是 Hotspot JVM 的实际实现,有时候人们一些部分说的是 JVM Specification,一部分说的是 Hotspot 实现,给人一种割裂感与误解。本篇主要从 Hotspot 实现出发,以 Linux x86 环境为主,紧密贴合 JVM 源码并且辅以各种 JVM 工具验证帮助大家理解 JVM 内存的结构。但是,本篇仅限于对于这些内存的用途,使用限制,相关参数的分析,有些地方可能比较深入,有些地方可能需要结合本身用这块内存涉及的 JVM 模块去说,会放在另一系列文章详细描述。最后,洗稿抄袭狗不得 house
本篇全篇目录(以及涉及的 JVM 参数):
从 Native Memory Tracking 说起(全网最硬核 JVM 内存解析 - 1.从 Native Memory Tracking 说起开始)
Native Memory Tracking 的开启
Native Memory Tracking 的使用(涉及 JVM 参数:
NativeMemoryTracking
)Native Memory Tracking 的 summary 信息每部分含义
Native Memory Tracking 的 summary 信息的持续监控
为何 Native Memory Tracking 中申请的内存分为 reserved 和 committed
JVM 内存申请与使用流程(全网最硬核 JVM 内存解析 - 2.JVM 内存申请与使用流程开始)
Linux 大页分配方式 - Huge Translation Lookaside Buffer Page (hugetlbfs)
Linux 大页分配方式 - Transparent Huge Pages (THP)
JVM 大页分配相关参数与机制(涉及 JVM 参数:
UseLargePages
,UseHugeTLBFS
,UseSHM
,UseTransparentHugePages
,LargePageSizeInBytes
)JVM commit 的内存与实际占用内存的差异
Linux 下内存管理模型简述
JVM commit 的内存与实际占用内存的差异
大页分配 UseLargePages(全网最硬核 JVM 内存解析 - 3.大页分配 UseLargePages开始)
Java 堆内存相关设计(全网最硬核 JVM 内存解析 - 4.Java 堆内存大小的确认开始)
验证
32-bit
压缩指针模式验证
Zero based
压缩指针模式验证
Non-zero disjoint
压缩指针模式验证
Non-zero based
压缩指针模式压缩对象指针存在的意义(涉及 JVM 参数:
ObjectAlignmentInBytes
)压缩对象指针与压缩类指针的关系演进(涉及 JVM 参数:
UseCompressedOops
,UseCompressedClassPointers
)压缩对象指针的不同模式与寻址优化机制(涉及 JVM 参数:
ObjectAlignmentInBytes
,HeapBaseMinAddress
)通用初始化与扩展流程
直接指定三个指标的方式(涉及 JVM 参数:
MaxHeapSize
,MinHeapSize
,InitialHeapSize
,Xmx
,Xms
)不手动指定三个指标的情况下,这三个指标(MinHeapSize,MaxHeapSize,InitialHeapSize)是如何计算的
压缩对象指针相关机制(涉及 JVM 参数:
UseCompressedOops
)(全网最硬核 JVM 内存解析 - 5.压缩对象指针相关机制开始)为何预留第 0 页,压缩对象指针 null 判断擦除的实现(涉及 JVM 参数:
HeapBaseMinAddress
)结合压缩对象指针与前面提到的堆内存限制的初始化的关系(涉及 JVM 参数:
HeapBaseMinAddress
,ObjectAlignmentInBytes
,MinHeapSize
,MaxHeapSize
,InitialHeapSize
)使用 jol + jhsdb + JVM 日志查看压缩对象指针与 Java 堆验证我们前面的结论
堆大小的动态伸缩(涉及 JVM 参数:
MinHeapFreeRatio
,MaxHeapFreeRatio
,MinHeapDeltaBytes
)(全网最硬核 JVM 内存解析 - 6.其他 Java 堆内存相关的特殊机制开始)适用于长期运行并且尽量将所有可用内存被堆使用的 JVM 参数 AggressiveHeap
JVM 参数 AlwaysPreTouch 的作用
JVM 参数 UseContainerSupport - JVM 如何感知到容器内存限制
JVM 参数 SoftMaxHeapSize - 用于平滑迁移更耗内存的 GC 使用
JVM 元空间设计(全网最硬核 JVM 内存解析 - 7.元空间存储的元数据开始)
jcmd <pid> VM.metaspace
元空间说明元空间相关 JVM 日志
元空间 JFR 事件详解
jdk.MetaspaceSummary
元空间定时统计事件jdk.MetaspaceAllocationFailure
元空间分配失败事件jdk.MetaspaceOOM
元空间 OOM 事件jdk.MetaspaceGCThreshold
元空间 GC 阈值变化事件jdk.MetaspaceChunkFreeListSummary
元空间 Chunk FreeList 统计事件CommitLimiter
的限制元空间可以 commit 的内存大小以及限制元空间占用达到多少就开始尝试 GC每次 GC 之后,也会尝试重新计算
_capacity_until_GC
首先类加载器 1 需要分配 1023 字节大小的内存,属于类空间
然后类加载器 1 还需要分配 1023 字节大小的内存,属于类空间
然后类加载器 1 需要分配 264 KB 大小的内存,属于类空间
然后类加载器 1 需要分配 2 MB 大小的内存,属于类空间
然后类加载器 1 需要分配 128KB 大小的内存,属于类空间
新来一个类加载器 2,需要分配 1023 Bytes 大小的内存,属于类空间
然后类加载器 1 被 GC 回收掉
然后类加载器 2 需要分配 1 MB 大小的内存,属于类空间
元空间的整体配置以及相关参数(涉及 JVM 参数:
MetaspaceSize
,MaxMetaspaceSize
,MinMetaspaceExpansion
,MaxMetaspaceExpansion
,MaxMetaspaceFreeRatio
,MinMetaspaceFreeRatio
,UseCompressedClassPointers
,CompressedClassSpaceSize
,CompressedClassSpaceBaseAddress
,MetaspaceReclaimPolicy
)元空间上下文
MetaspaceContext
虚拟内存空间节点列表
VirtualSpaceList
虚拟内存空间节点
VirtualSpaceNode
与CompressedClassSpaceSize
MetaChunk
类加载的入口
SystemDictionary
与保留所有ClassLoaderData
的ClassLoaderDataGraph
每个类加载器私有的
ClassLoaderData
以及ClassLoaderMetaspace
管理正在使用的
MetaChunk
的MetaspaceArena
元空间内存分配流程(全网最硬核 JVM 内存解析 - 9.元空间内存分配流程开始)
ClassLoaderData
回收ChunkHeaderPool
池化MetaChunk
对象ChunkManager
管理空闲的MetaChunk
类加载器到
MetaSpaceArena
的流程从
MetaChunkArena
普通分配 - 整体流程从
MetaChunkArena
普通分配 -FreeBlocks
回收老的current chunk
与用于后续分配的流程从
MetaChunkArena
普通分配 - 尝试从FreeBlocks
分配从
MetaChunkArena
普通分配 - 尝试扩容current chunk
从
MetaChunkArena
普通分配 - 从ChunkManager
分配新的MetaChunk
从
MetaChunkArena
普通分配 - 从ChunkManager
分配新的MetaChunk
- 从VirtualSpaceList
申请新的RootMetaChunk
从
MetaChunkArena
普通分配 - 从ChunkManager
分配新的MetaChunk
- 将RootMetaChunk
切割成为需要的MetaChunk
MetaChunk
回收 - 不同情况下,MetaChunk
如何放入FreeChunkListVector
什么时候用到元空间,以及释放时机
元空间保存什么
什么是元数据,为什么需要元数据
什么时候用到元空间,元空间保存什么
元空间的核心概念与设计(全网最硬核 JVM 内存解析 - 8.元空间的核心概念与设计开始)
元空间分配与回收流程举例(全网最硬核 JVM 内存解析 - 10.元空间分配与回收流程举例开始)
元空间大小限制与动态伸缩(全网最硬核 JVM 内存解析 - 11.元空间分配与回收流程举例开始)
jcmd VM.metaspace
元空间说明、元空间相关 JVM 日志以及元空间 JFR 事件详解(全网最硬核 JVM 内存解析 - 12.元空间各种监控手段开始)JVM 线程内存设计(重点研究 Java 线程)(全网最硬核 JVM 内存解析 - 13.JVM 线程内存设计开始)
解释执行与编译执行时候的判断(x86为例)
一个 Java 线程 Xss 最小能指定多大
JVM 中有哪几种线程,对应线程栈相关的参数是什么(涉及 JVM 参数:
ThreadStackSize
,VMThreadStackSize
,CompilerThreadStackSize
,StackYellowPages
,StackRedPages
,StackShadowPages
,StackReservedPages
,RestrictReservedStack
)Java 线程栈内存的结构
Java 线程如何抛出的 StackOverflowError
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
意思是:
一共 1383 个类加载器,加载了 33006 个类(其中 1361 个是共享类)。
capacity 是指
MetaChunk
的总容量大小(Reserved
内存);committed 是指这些MetaChunk
中committed
的内存大小,也就是实际占用系统物理内存是这么大(虽然可能会有点细微差异,参考本篇文章的第二章);used 是指这些MetaChunk
实际使用的大小,肯定比committed
的要小;free 是指剩余的大小;committed = used + free + waste;deallocated 是指回收到FreeBlocks
的大小,属于 free 的一部分,另一部分就是MetaChunk
中committed
但是还没使用的部分;waste 是指浪费的大小(前面我们提到了什么造成的浪费,主要是搜索FreeBlocks
的空间使用的时候,可能正好剩下 1 字节,就不放回了继续使用了)洗稿的狗也遇到不少数据元空间使用情况:一共使用了 7964 个
MetaChunk
,这些MetaChunk
相关总容量大小是150.83 MB
,目前commit
了150.77 MB
,使用了150.21 MB
,剩余562.77 KB
可以使用,6.65 KB
的空间被浪费了。FreeBlocks
目前回收了869
块内存,一共249.52 KB
。类元空间使用情况:一共使用了 2546 个
MetaChunk
,总容量大小是21.00 MB
,目前commit
了20.93 MB
,使用了20.21 MB
,剩余741.42 KB
可以使用,216 bytes
的空间被浪费了。FreeBlocks
目前回收了1057
块内存,一共264.88 KB
。总的元空间使用情况(类元空间 + 数据元空间的):一共使用了 10510 个
MetaChunk
,总容量大小是171.83 MB
,目前commit
了171.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.
意思是:
数据元空间的
VirtualSpaceList
:总共Reserve
了152.00 MB
,目前Commit
了150.81 MB
,一共有19
个VirtualSpaceNode
。这个与MetaChunk
的统计信息是有差异的,VirtualSpaceList
的统计信息更体现元空间实际占用的,从MetaChunk
角度统计的时候,将每个MetaChunk
统计信息相加,会有精度损失。类元空间的
VirtualSpaceList
:总共Reserve
了1.00 GB
,目前Commit
了20.94 MB
,一共有1
个VirtualSpaceNode
。总的元空间的
VirtualSpaceList
:总共Reserve
了1.15 GB
,目前Commit
了171.75 MB
。不要偷取他人的劳动成果,也不要浪费自己的时间和精力,让我们一起做一个有良知的写作者。
接下来是每个 ChunkManager
的 FreeChunkListVector
的统计信息:
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%)
以上的信息可能用图片更直接一些:

接下来是关于回收利用的从 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.
包含的信息是:
当前被使用的
MetaChunk
(即存在于每个类加载器对应的MetaspaceArena
中的MetaChunk
)中有6.86 KB
的空间被浪费了。当前被使用的MetaChunk
(即存在于每个类加载器对应的MetaspaceArena
中的MetaChunk
)中剩余1.27 MB
可以使用。在FreeChunkListVector
中没有浪费的空间,其实从前面的FreeChunkListVector
的详细信息就能看出来。FreeBlocks
目前回收了1926
块内存,一共514.41 KB
。FreeBlocks
里面有1926
个FreeBlock
,一共514.41 KB
。ChunkHeaderPool
目前有10520
个ChunkHeader
,一共占用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.
包含的信息是:
num_allocs_failed_limit
:元空间普通分批内存失败的次数(前文分析过详细流程),后面也有对应的 JFR 事件会分析。num_arena_births
:MetaspaceArena
的创建次数。num_arena_deaths
:MetaspaceArena
的销毁次数。发生于对应的类加载器被回收之后。num_vsnodes_births
:VirtualSpaceNode
的创建次数。(根据前面的VirtualSpaceList
的统计信息可以知道是 19 + 1 = 20)num_vsnodes_deaths
:VirtualSpaceNode
的销毁次数。num_space_committed
:Commit
内存的次数。num_space_uncommitted
:Uncommit
内存的次数。num_chunks_returned_to_freelist
:MetaChunk
被回收到FreeChunkListVector
的次数。num_chunks_taken_from_freelist
:从FreeChunkListVector
中获取MetaChunk
进行分配的次数。num_chunk_merges
:MetaChunk
合并的次数。num_chunk_splits
:MetaChunk
拆分的次数。num_chunks_enlarged
:MetaChunk
扩容的次数。num_purges
:MetaspaceArena
的清理次数。一般等于销毁次数。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.
MaxMetaspaceSize
:元空间最大值。默认是无限制的。这里我们也没限制。CompressedClassSpaceSize
:压缩类空间大小。默认是 1 GB。这里我们也没指定,所以是默认的。Initial GC threshold
:初始的元空间 GC 阈值。默认是 40 MB。这里我们也没指定,所以是默认的。Current GC threshold
:当前的元空间 GC 阈值。前面我们分析过这个阈值改变的机制。CDS
:是否开启了 CDS。默认开启。这个我们不用太关心,主要和 CDS 特性相关(JEP 310: Application Class-Data Sharing 和 JEP 350: Dynamic CDS Archives),在以后的文章会详细分析。元空间
MetaspaceReclaimPolicy
为balanced
commit 粒度(
commit_granule_bytes
)为 65536 字节,转化单位为字之后,是 8192 字(一 word 为 8 字节)。虚拟内存空间节点内存大小(virtual_space_node_default_size
)为 1048576 字,转化单位为字之后,是 64 MB。当前 MetaChunk 不足以分配的时候,是否尝试扩容当前MetaChunk
(enlarge_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.
以上这几行日志的意思是:元空间 MetaspaceReclaimPolicy
为 balanced
,commit 粒度(commit_granule_bytes
)为 65536 字节,转化单位为字之后,是 8192 字(一 word 为 8 字节)。虚拟内存空间节点内存大小(virtual_space_node_default_size
)为 1048576 字,转化单位为字之后,是 64 MB。当前 MetaChunk 不足以分配的时候,是否尝试扩容当前 MetaChunk
(enlarge_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).
这几行日志的意思是:
CDS 元数据映射到内存的地址范围是
[0x0000000800000000-0x0000000800bde000-0x0000000800bde000)
,大小为 12443648 字节,共享基地址为0x0000000800000000
,ArchiveRelocationMode
为关闭。这些信息我们不用太关心,主要和 CDS 特性相关(JEP 310: Application Class-Data Sharing 和 JEP 350: Dynamic CDS Archives),在以后的文章会详细分析。我们这里是默认配置,所以压缩类空间是开启的,初始化压缩类空间,映射到内存的地址范围是
[0x0000000800c00000-0x0000000840c00000)
,Reserved 内存大小为 1073741824 字节(1GB),默认压缩类空间最大大小就是 1GB。加载到压缩类空间的类的基地址为0x0000000800000000
(),偏移量为 0,范围为0x100000000
,这个前面也简单分析过。Bootstrap ClassLoader
创建了两个MetaspaceArena
,分别是前文分析的类元空间的MetaspaceArena
和数据元空间的MetaspaceArena
,放入对应的ClassLoadMetaSpace
中。不要偷取他人的劳动成果,也不要浪费自己的时间和精力,让我们一起做一个有良知的写作者。初始化类元空间的还有数据元空间的
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.
这几行日志的意思分别是:
加载类需要从元空间申请内存,这是第一次申请,所以各个数据结构都是空的,所以需要申请新的
MetaChunk
,优先考虑的与最大的ChunkLevel
都是12
,对应 1KB。本次申请发生在ChunkManager @0x0000ffff807863d0
申请新的
RootMetaChunk
,基址0x0000000800c00000
将新的
RootMetaChunk
按照之前的算法拆分到ChunkLevel
为12
,结果是MetaChunk @0x0000ffff807867f0
,将拆出来的其他MetaChunk
放入ChunkManager @0x0000ffff807863d0
的FreeListVector
中
4.6.3. 元空间 JFR 事件详解
4.6.3.1. jdk.MetaspaceSummary
元空间定时统计事件
元空间定时统计事件 jdk.MetaspaceSummary
,包括以下属性:
事件开始时间:其实就是事件发生时间
GC Identifier:全局 GC 的 id 标识
When:事件发生的时机,包括
Before GC
和After 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:整个元空间实际保存数据使用的内存空间大小(其实就是类元空间 + 数据元空间)

4.6.3.2. jdk.MetaspaceAllocationFailure
元空间分配失败事件
前面提到过,如果普通分配失败,那么会触发 jdk.MetaspaceAllocationFailure
这个 JFR 事件,大家可以监控这个事件,去调整元空间大小减少由于元空间不足触发的 GC,这个事件包括以下属性:
事件开始时间:其实就是事件发生时间
类加载器:触发 OOM 的类加载器
Hidden Class Loader:是否是隐藏类加载器
Metadata Type:元数据类型,分为属于类元空间的以及属于数据元空间的两种类型,分别是:
Class
和Metadata
Metaspace Object Type:元空间对象类型,包括
Class
、ConstantPool
、Symbol
、Method
、Klass
、Module
、Package
、Other
Size:本次分配的大小
这个事件也会采集堆栈信息,用来定位分配失败的源头是哪些类的加载导致的。

4.6.3.3. jdk.MetaspaceOOM
元空间 OOM 事件
前面提到过,当元空间 OOM 的时候,就会产生这个事件,这个事件包括以下属性(和 jdk.MetaspaceAllocationFailure
事件一样):
事件开始时间:其实就是事件发生时间
类加载器:触发 OOM 的类加载器
Hidden Class Loader:是否是隐藏类加载器
Metadata Type:元数据类型,分为属于类元空间的以及属于数据元空间的两种类型,分别是:
Class
和Metadata
Metaspace Object Type:元空间对象类型,包括
Class
、ConstantPool
、Symbol
、Method
、Klass
、Module
、Package
、Other
Size:本次分配的大小
与 jdk.MetaspaceAllocationFailure
事件一样,也会采集堆栈信息,用来定位 OOM 的原因。

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
进行分配。对应的Updater
是expand_and_allocate
每次 GC 之后,触发重新计算
_capacity_until_GC
,如果有更新,就会生成这个事件,对应的Updater
是compute_new_size

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);
}

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