个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判。如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 issue,谢谢支持~
另外,本文为了避免抄袭,会在不影响阅读的情况下,在文章的随机位置放入对于抄袭和洗稿的人的“亲切”的问候。如果是正常读者看到,笔者在这里说声对不起,。如果被抄袭狗或者洗稿狗看到了,希望你能够好好反思,不要再抄袭了,谢谢。
今天又是干货满满的一天,这是全网最硬核 JVM 解析系列第四篇,往期精彩:
本篇是关于 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 下内存管理模型简述
- JVM commit 的内存与实际占用内存的差异
- JVM commit 的内存与实际占用内存的差异
- 大页分配 UseLargePages(全网最硬核 JVM 内存解析 - 3.大页分配 UseLargePages开始)
- Linux 大页分配方式 - Huge Translation Lookaside Buffer Page (hugetlbfs)
- Linux 大页分配方式 - Transparent Huge Pages (THP)
- JVM 大页分配相关参数与机制(涉及 JVM 参数:
UseLargePages
,UseHugeTLBFS
,UseSHM
,UseTransparentHugePages
,LargePageSizeInBytes
)
- Java 堆内存相关设计(全网最硬核 JVM 内存解析 - 4.Java 堆内存大小的确认开始)
- 通用初始化与扩展流程
- 直接指定三个指标的方式(涉及 JVM 参数:
MaxHeapSize
,MinHeapSize
,InitialHeapSize
,Xmx
,Xms
) - 不手动指定三个指标的情况下,这三个指标(MinHeapSize,MaxHeapSize,InitialHeapSize)是如何计算的
- 压缩对象指针相关机制(涉及 JVM 参数:
UseCompressedOops
)(全网最硬核 JVM 内存解析 - 5.压缩对象指针相关机制开始)- 压缩对象指针存在的意义(涉及 JVM 参数:
ObjectAlignmentInBytes
) - 压缩对象指针与压缩类指针的关系演进(涉及 JVM 参数:
UseCompressedOops
,UseCompressedClassPointers
) - 压缩对象指针的不同模式与寻址优化机制(涉及 JVM 参数:
ObjectAlignmentInBytes
,HeapBaseMinAddress
)
- 压缩对象指针存在的意义(涉及 JVM 参数:
- 为何预留第 0 页,压缩对象指针 null 判断擦除的实现(涉及 JVM 参数:
HeapBaseMinAddress
) - 结合压缩对象指针与前面提到的堆内存限制的初始化的关系(涉及 JVM 参数:
HeapBaseMinAddress
,ObjectAlignmentInBytes
,MinHeapSize
,MaxHeapSize
,InitialHeapSize
) - 使用 jol + jhsdb + JVM 日志查看压缩对象指针与 Java 堆验证我们前面的结论
- 验证
32-bit
压缩指针模式 - 验证
Zero based
压缩指针模式 - 验证
Non-zero disjoint
压缩指针模式 - 验证
Non-zero based
压缩指针模式
- 验证
- 堆大小的动态伸缩(涉及 JVM 参数:
MinHeapFreeRatio
,MaxHeapFreeRatio
,MinHeapDeltaBytes
)(全网最硬核 JVM 内存解析 - 6.其他 Java 堆内存相关的特殊机制开始) - 适用于长期运行并且尽量将所有可用内存被堆使用的 JVM 参数 AggressiveHeap
- JVM 参数 AlwaysPreTouch 的作用
- JVM 参数 UseContainerSupport - JVM 如何感知到容器内存限制
- JVM 参数 SoftMaxHeapSize - 用于平滑迁移更耗内存的 GC 使用
- JVM 元空间设计(全网最硬核 JVM 内存解析 - 7.元空间存储的元数据开始)
- 什么是元数据,为什么需要元数据
- 什么时候用到元空间,元空间保存什么
- 2.1. 什么时候用到元空间,以及释放时机
- 2.2. 元空间保存什么
- 元空间的核心概念与设计(全网最硬核 JVM 内存解析 - 8.元空间的核心概念与设计开始)
- 元空间的整体配置以及相关参数(涉及 JVM 参数:
MetaspaceSize
,MaxMetaspaceSize
,MinMetaspaceExpansion
,MaxMetaspaceExpansion
,MaxMetaspaceFreeRatio
,MinMetaspaceFreeRatio
,UseCompressedClassPointers
,CompressedClassSpaceSize
,CompressedClassSpaceBaseAddress
,MetaspaceReclaimPolicy
) - 元空间上下文
MetaspaceContext
- 虚拟内存空间节点列表
VirtualSpaceList
- 虚拟内存空间节点
VirtualSpaceNode
与CompressedClassSpaceSize
MetaChunk
ChunkHeaderPool
池化MetaChunk
对象ChunkManager
管理空闲的MetaChunk
- 类加载的入口
SystemDictionary
与保留所有ClassLoaderData
的ClassLoaderDataGraph
- 每个类加载器私有的
ClassLoaderData
以及ClassLoaderMetaspace
- 管理正在使用的
MetaChunk
的MetaspaceArena
- 元空间内存分配流程(全网最硬核 JVM 内存解析 - 9.元空间内存分配流程开始)
- 类加载器到
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
- 类加载器到
ClassLoaderData
回收
- 元空间的整体配置以及相关参数(涉及 JVM 参数:
- 元空间分配与回收流程举例(全网最硬核 JVM 内存解析 - 10.元空间分配与回收流程举例开始)
- 首先类加载器 1 需要分配 1023 字节大小的内存,属于类空间
- 然后类加载器 1 还需要分配 1023 字节大小的内存,属于类空间
- 然后类加载器 1 需要分配 264 KB 大小的内存,属于类空间
- 然后类加载器 1 需要分配 2 MB 大小的内存,属于类空间
- 然后类加载器 1 需要分配 128KB 大小的内存,属于类空间
- 新来一个类加载器 2,需要分配 1023 Bytes 大小的内存,属于类空间
- 然后类加载器 1 被 GC 回收掉
- 然后类加载器 2 需要分配 1 MB 大小的内存,属于类空间
- 元空间大小限制与动态伸缩(全网最硬核 JVM 内存解析 - 11.元空间分配与回收流程举例开始)
CommitLimiter
的限制元空间可以 commit 的内存大小以及限制元空间占用达到多少就开始尝试 GC- 每次 GC 之后,也会尝试重新计算
_capacity_until_GC
jcmd VM.metaspace
元空间说明、元空间相关 JVM 日志以及元空间 JFR 事件详解(全网最硬核 JVM 内存解析 - 12.元空间各种监控手段开始)jcmd <pid> VM.metaspace
元空间说明- 元空间相关 JVM 日志
- 元空间 JFR 事件详解
jdk.MetaspaceSummary
元空间定时统计事件jdk.MetaspaceAllocationFailure
元空间分配失败事件jdk.MetaspaceOOM
元空间 OOM 事件jdk.MetaspaceGCThreshold
元空间 GC 阈值变化事件jdk.MetaspaceChunkFreeListSummary
元空间 Chunk FreeList 统计事件
- JVM 线程内存设计(重点研究 Java 线程)(全网最硬核 JVM 内存解析 - 13.JVM 线程内存设计开始)
- JVM 中有哪几种线程,对应线程栈相关的参数是什么(涉及 JVM 参数:
ThreadStackSize
,VMThreadStackSize
,CompilerThreadStackSize
,StackYellowPages
,StackRedPages
,StackShadowPages
,StackReservedPages
,RestrictReservedStack
) - Java 线程栈内存的结构
- Java 线程如何抛出的 StackOverflowError
- 解释执行与编译执行时候的判断(x86为例)
- 一个 Java 线程 Xss 最小能指定多大
- JVM 中有哪几种线程,对应线程栈相关的参数是什么(涉及 JVM 参数:
4.3. 元空间的核心概念与设计
4.3.1. 元空间的整体配置以及相关参数
元空间配置相关的参数:
MetaspaceSize
:初始元空间大小,也是最小元空间大小。后面元空间大小伸缩的时候,不会小于这个大小。默认是 21M。抄袭剽窃侵权
滚MaxMetaspaceSize
:最大元空间大小,默认是无符号 int 最大值。MinMetaspaceExpansion
:每次元空间大小伸缩的时候,至少改变的大小。默认是 256K。后文讲到元空间内存大小限制的时候会详细分析。MaxMetaspaceExpansion
:每次元空间大小伸缩的时候,最多改变的大小。默认是 4M。后文讲到元空间内存大小限制的时候会详细分析。MaxMetaspaceFreeRatio
:最大元空间空闲比例,默认是 70,即 70%。后文讲到元空间内存大小限制的时候会详细分析。MinMetaspaceFreeRatio
:最小元空间空闲比例,默认是 40,即 40%。后文讲到元空间内存大小限制的时候会详细分析。UseCompressedClassPointers
:前文提到过,是否开启压缩类指针。默认是开启的。老版本中,UseCompressedClassPointers
取决于UseCompressedOops
,即压缩对象指针如果没开启,那么压缩类指针也无法开启。但是从 Java 15 Build 23 开始,UseCompressedClassPointers
已经不再依赖UseCompressedOops
了,两者在大部分情况下已经独立开来。除非在 x86 的 CPU 上面启用 JVM Compiler Interface(例如使用 GraalVM)。参考 JDK ISSUE:bugs.openjdk.java.net/browse/JDK-…CompressedClassSpaceSize
:如果启用了压缩类指针,则元空间会分为类元空间和数据元空间,否则只有数据元空间。这个参数限制类元空间的大小,范围是 1M ~ 3G。默认大小是 1G,如果指定了MaxMetaspaceSize
,那么为 1G 与MaxMetaspaceSize * 0.8
中比较小的那个值,CompressedClassSpaceBaseAddress
:类元空间起始虚拟内存地址,这个一般不指定。作用和前文分析堆内存的堆起始位置的作用差不多。MetaspaceReclaimPolicy
:可以为:balanced
,aggressive
, 以及none
,需要注意一点的是none
要被移除了(bugs.openjdk.org/browse/JDK-…)。默认是balanced
。具体主要是影响元空间底层相关的配置,下面我们会详细分析。
元空间底层相关的配置包括:
- commit 粒度 - commit_granule:通过第二章的分析我们知道,JVM 的空间一般是先 reserve, 之后 commit 之前 reserve 的空间的一部分,然后才能使用的。这个 commit 粒度代表元空间中 commit 内存的最小粒度,元空间在扩容缩容的时候最小的大小单位是 commit 粒度。
- 虚拟内存空间节点内存大小 - virtual_space_node_default_word_size:这是后文我们会详细分析的
VirtualSpaceNode
的虚拟内存大小。大小在 64 位环境下是 64 MB。 - 虚拟内存空间节点内存对齐 - virtual_space_node_reserve_alignment_words:这是后文我们会详细分析的
VirtualSpaceNode
的虚拟内存大小需要对齐的大小,即整体大小需要大于这个对齐大小并且是这个对齐大小整数倍。这个大小就是MetaChunk
的最大大小,即 4MB。 - 当前 MetaChunk 不足以分配的时候,是否尝试扩容当前 MetaChunk - enlarge_chunks_in_place:这个参数在正式 JVM 中是 true,并且不能修改。后文我们会详细分析什么是
MetaChunk
。这里简单理解就是,元空间整体使用了和 Linux 伙伴分配算法类似的设计与抽象,其中内存分配的单元就是 Chunk,元空间中对应的就是 MetaChunk。 - 分配新的 MetaChunk 的时候,是否一下子 commit MetaChunk 所有的内存 - new_chunks_are_fully_committed:后文我们会详细分析什么是
MetaChunk
。 - 在 MetaChunk 整个空间都没有使用的时候,是否将 MetaChunk 的内存全部释放回操作系统 - uncommit_free_chunks:后文我们会详细分析什么是
MetaChunk
。
从 Java 16 开始,引入了弹性元空间。老的元空间由于设计上分配粒度比较大,并且没有很好地释放空间的策略设计,所以占用可能比较大。Java 16 开始,JEP 387: Elastic Metaspace 引入了弹性元空间的设计,也是我们这里要讨论的设计。这个弹性元空间也引入了一个重要的参数 -XX:MetaspaceReclaimPolicy
。
MetaspaceReclaimPolicy
:可以为:balanced
, aggressive
, 以及 none
,需要注意一点的是 none
要被移除了(bugs.openjdk.org/browse/JDK-…),这三个配置具体影响是:
4.3.2. 元空间上下文 MetaspaceContext
MetaspaceContext
本身直接原生堆上面分配,Native Memory Tracking 中属于 Metaspace
那一类别,即元空间的抽象类占用的空间。
1 | kotlin复制代码class MetaspaceContext : public CHeapObj<mtMetaspace> |
JVM 元空间,会在全局建立两个元空间上下文(MetaspaceContext
),一个用于类元空间(我们后面称为类元空间 MetaspaceContext
),一个用于数据元空间(我们后面称为数据元空间 MetaspaceContext
)。当然,在没有启用压缩类指针的时候,只会初始化一个数据元空间 MetaspaceContext
,不会初始化类元空间 MetaspaceContext
,之后使用分配的时候,也只会用数据元空间 MetaspaceContext
进行分配。但是我们在后面讨论的时候,只会讨论开启压缩类指针的情况,因为这是默认并且常用的情况。
每个 MetaspaceContext
都会对应一个独立的 VirtualSpaceList
,以及一个独立的 ChunkManager
。
这个 VirtualSpaceList
中的每一个元素都是一个 VirtualSpaceNode
。顾名思义,VirtualSpaceNode
是从操作系统申请内存,与元空间内存划分的抽象隔离的中间层抽象。VirtualSpaceList
负责与操作系统交互,申请或者释放内存。元空间与 VirtualSpaceList
交互,使用内存。
ChunkManager
顾名思义,是管理所有 Chunk 的内存管理器。Chunk 这个概念经常出现在各种伙伴内存管理算法框架(Buddy Allocator)中,一般指内存管理分配的最小单元,这里的 Chunk 抽象对应的就是 MetaChunk
。ChunkManager
从 VirtualSpaceList
上面获取一块连续比较大的内存的 MetaChunk
(其实是 RootMetaChunk
),然后将这个 RootMetaChunk
按照分配需求,连续对半分割成需要的大小,返回这个合适大小的 MetaChunk
,剩下的分割出来的 MetaChunk
进入 FreeChunkListVector
用于下次分配 MetaChunk
的时候,直接返回合适的,就不再从 VirtualSpaceList
获取了。
我们接下来仔细分析 VirtualSpaceList
与 ChunkManager
4.3.3. 虚拟内存空间节点列表 VirtualSpaceList
VirtualSpaceList
本身直接原生堆上面分配,Native Memory Tracking 中属于 Class
那一类别,即元空间的加载类占用的空间。其实本人感觉这么设计不太合理,应该和 MetaspaceContext
属于同一个类别才比较合理。真正分配加载的类的占用空间的是从 VirtualSpaceNode
上面标记的内存分配的,这是下一小节要分析的内容。
1 | kotlin复制代码class VirtualSpaceList : public CHeapObj<mtClass> |
首先提一点,类元空间 MetaspaceContext
与数据元空间 MetaspaceContext
略有不同:类元空间 MetaspaceContext
的 VirtualSpaceList
是不可以扩展申请新的内存的,但是数据元空间 MetaspaceContext
的 VirtualSpaceList
是可以的。也就是说:类元空间 MetaspaceContext
的 VirtualSpaceList
其实只有一个 VirtualSpaceNode
,但是数据元空间 MetaspaceContext
的 VirtualSpaceList
是一个包含多个 VirtualSpaceNode
的列表。
4.3.4. 虚拟内存空间节点 VirtualSpaceNode
与 CompressedClassSpaceSize
VirtualSpaceNode
本身直接原生堆上面分配,Native Memory Tracking 中属于 Class
那一类别,即元空间的加载类占用的空间。其实本人感觉这么设计不太合理,应该和 MetaspaceContext
属于同一个类别才比较合理。真正分配加载的类的占用空间的是从 VirtualSpaceNode
上面标记的内存地址分配的,VirtualSpaceNode
本身的空间占用只是起到描述记录作用,应该也属于元空间描述的那一类。
1 | kotlin复制代码class VirtualSpaceNode : public CHeapObj<mtClass> |
VirtualSpaceNode
是一块连续的虚拟内存空间内存的抽象。类元空间的 VirtualSpaceList
只包含一个 VirtualSpaceNode
,大小是前文提到的 CompressedClassSpaceSize
。
数据元空间并不像类元空间或者堆内存那样,一下子 reserve 最大堆内存限制的内存,而是每次 reserve VirtualSpaceNode
大小。VirtualSpaceNode
大小在 64 位环境下是 64 MB:
1 | arduino复制代码static const size_t _virtual_space_node_default_word_size = |
VirtualSpaceNode
通过两个数据结构来管理它维护的虚拟内存空间:
CommitMask
:实际是一个位图,用于维护哪些内存被 commit 了,哪些没有,位图的标记的单位就是前文提到的 commit_granule(commit 粒度)。RootChunkAreaLUT
:用于维护每个RootMetaChunk
的内存分布。至于什么是RootMetaChunk
在后续我们讲MetaChunk
的时候会详细讲解。
一个 VirtualSpaceNode
的主要结构如下图所示:
4.3.5. MetaChunk
MetaChunk
是元空间内存分配的核心抽象,其本质就是描述一块连续的虚拟内存空间。MetaChunk
本身只是一个描述对象,它也是直接原生堆上面分配,Native Memory Tracking 中属于 Metaspace
那一类别,即元空间的抽象类占用的空间。这个描述对象是池化的,参考后面会分析的 ChunkHeaderPool
。不要偷取他人的劳动成果!
元空间的任意分配,都是在某个 MetaChunk
上进行的(不要偷取他人的劳动成果!)。MetaChunk
有级别的概念,即 ChunkLevel
,每个 MetaChunk
都有自己的 ChunkLevel
,这个 ChunkLevel
主要代表了 MetaChunk
描述的内存空间的大小,每一个 level 都是下一个 level 大小的 2 倍:
从 VirtualSpaceNode
上直接划分的 MetaChunk
是 RootMetaChunk
,它的 ChunkLevel
为最高级别的 0,大小是 4MB,并且其中的内存只是 reserve 还没有 commit 的。
MetaChunk
有三个状态:
Dead
:即MetaChunk
只是对象被创建出来,但是没有关联描述实际的虚拟内存。后面我们会知道,MetaChunk
是池化可回收在利用的,MetaChunk
的池就是ChunkHeaderPool
。位于ChunkHeaderPool
都还没有关联描述实际的虚拟内存,状态为Dead
。Free
:即MetaChunk
关联描述了实际的虚拟内存,但是没有被实际使用。此时,这个MetaChunk
位于ChunkManager
管理。InUse
:即MetaChunk
关联描述了实际的虚拟内存,也被实际使用了,此时,MetaChunkArena
管理这个MetaChunk
上面的内存分配。
4.3.5.1. ChunkHeaderPool
池化 MetaChunk
对象
MetaChunk
实际上只是一块连续的虚拟内存空间的描述类(不要偷取他人的劳动成果!),即元数据类。由于类加载需要的大小不一,并且还经常会发生合并,切分等等,MetaChunk
可能有很多很多,元空间为了节省这个元数据类占用的空间,将其池化,回收再利用。这个池就是 ChunkHeaderPool
。例如,从 VirtualSpaceNode
上直接划分 RootMetaChunk
的内存空间,会从 ChunkHeaderPool
申请一个 MetaChunk
用于描述。当两个 MetaChunk
的空间需要合并成一个的时候,其中一个 MetaChunk
其实就没有用了,会放回 ChunkHeaderPool
,而不是直接 free 掉这个对象。
ChunkHeaderPool
本身直接原生堆上面分配,Native Memory Tracking 中属于 Metaspace
那一类别,即元空间的抽象类占用的空间。
1 | kotlin复制代码class ChunkHeaderPool : public CHeapObj<mtMetaspace> |
其实从这里我们可以推测出,MetaChunk
本身也是直接原生堆上面分配,Native Memory Tracking 中也是属于 Metaspace
那一类别。
ChunkHeaderPool
的结构是:
其实 ChunkHeaderPool
的机制很简单:
- 申请
MetaChunk
用于描述内存:- 首先查看
_freelist
,是否有之前放回的MetaChunk
可以使用,如果有,就返回那个MetaChunk
,并从_freelist
移除这个MetaChunk
- 如果没有,读取
_current_slab
指向的Slab
,Slab
核心就是一个预分配好的MetaChunk
数组(大小是 128),_top
指的是当前使用到数组的哪一个。 - 如果
_top
没有到 128,返回_top
代表的MetaChunk
,并将_top
加 1。 - 如果
_top
到 128,创建新的Slab
,_current_slab
指向这个新的Slab
- 首先查看
- 回收
MetaChunk
:放入_freelist
4.3.5.2. ChunkManager
管理空闲的 MetaChunk
ChunkManager
本身直接原生堆上面分配,Native Memory Tracking 中属于 Metaspace
那一类别,即元空间的抽象类占用的空间。不要偷取他人的劳动成果!
1 | kotlin复制代码class ChunkManager : public CHeapObj<mtMetaspace> |
https://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/memory/metaspace/chunkManager.hpp
ChunkManager
管理已经关联内存但是还没使用(状态是 Free
)的 MetaChunk
。在第一次从 VirtualSpaceNode
上面分配 RootMetaChunk
的内存的时候,根据申请的内存大小,决定要将 RootMetaChunk
拆分到某个 ChunkLevel
大小之后用于当前分配,拆分出来的其他的 MetaChunk
还没有使用,先放入一个类似于之前 ChunkHeaderPool
里面的 _free_list
的结构,用于下次申请 MetaChunk
用于分配的时候,先从这个里面找,找不到之后再从 VirtualSpaceNode
上面尝试分配新的 RootMetaChunk
。不要惯着cao袭的人!
ChunkManager
的整体结构是:
ChunkManager
主要维护一个 FreeChunkListVector
,FreeChunkListVector
里面是一个 FreeChunkList
数组(还有xigao dog 的码)。FreeChunkList
是一个 MetaChunk
链表,链表中都是 Free
的 MetaChunk
,同样 ChunkLevel
的 MetaChunk
位于同一个 FreeChunkList
中。FreeChunkList
数组以 ChunkLevel
为下标,这样的数据结构可以快速找到一个所需 ChunkLevel
的 MetaChunk
。FreeChunkList
这个链表其实是一个双向链表,包含头尾两个指针,如果一个 MetaChunk
管理的内存被 commit 了,就会放在链表头部,没有 commit 的放在链表尾部。
MetaChunk
具体的分配,切分,合并流程,我们会在介绍完 MetaspaceArena
之后详细分析。但是,MetaspaceArena
和 ChunkManager
不一样,ChunkManager
是全局两个,一个属于类元空间,一个属于数据元空间,倘若没有开启压缩类指针,那么就只有一个数据元空间 ChunkManager
,而 MetaspaceArena
我们后面会看到是每个 ClassLoader
独立私有的。所以,在讲 MetaspaceArena
之前,我们先要从另一个角度即 ClassLoader
加载类的角度出发,向下一层一层剖析到 MetaspaceArena
。
4.3.6. 类加载的入口 SystemDictionary
与保留所有 ClassLoaderData
的 ClassLoaderDataGraph
类加载的入口在全局唯一的 SystemDictionary
中,这里我们只是为了看一下类加载需要哪些参数,来搞清楚对应关系,不用关心细节,入口代码是:
https://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/classfile/systemDictionary.cpp
1 | cpp复制代码InstanceKlass* SystemDictionary::resolve_from_stream(ClassFileStream* st, |
可以看到,加载类需要以下参数:
ClassFileStream* st
:类文件流Symbol* class_name
:加载的类的名称Handle class_loader
:是哪个类加载器const ClassLoadInfo& cl_info
:类加载器信息
在加载类的时候,SystemDictionary
会获取类加载器的 ClassLoaderData
,ClassLoaderData
是每个类加载器私有的。
https://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/classfile/systemDictionary.cpp
1 | php复制代码//通过类加载器获取对应的 `ClassLoaderData` |
ClassLoaderDataGraph
保存着所有的 ClassLoaderData
,这个主要用来遍历每个类加载器,以及获取每个类加载器加载的类的信息,还有遍历类加载器加载的类,例如 jcmd
命令中的 VM.classloaders
以及 VM.classloader_stats
就是这么实现的。但是,我们就不纠结于 ClassLoaderDataGraph
的细节了,这不是咱们的重点。
4.3.7. 每个类加载器私有的 ClassLoaderData
以及 ClassLoaderMetaspace
ClassLoaderData
本身直接原生堆上面分配,Native Memory Tracking 中属于 Class
那一类别,即元空间的加载类占用的空间。这就很合理了,不加载类就不会有 ClassLoaderData
。
https://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/classfile/classLoaderData.hpp
1 | cpp复制代码 class ClassLoaderData : public CHeapObj<mtClass> |
如前所述,ClassLoaderData
是每个类加载器私有的。ClassLoaderData
包含的元素众多,我们这里只关心它其中与元空间内存分配相关的,即 ClassLoaderMetaspace
:
https://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/classfile/classLoaderData.hpp
1 | cpp复制代码 ClassLoaderMetaspace * volatile _metaspace; |
ClassLoaderMetaspace
本身直接原生堆上面分配,Native Memory Tracking 中属于 Class
那一类别,即元空间的加载类占用的空间。
https://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/memory/classLoaderMetaspace.hpp
1 | cpp复制代码 class ClassLoaderMetaspace : public CHeapObj<mtClass> |
ClassLoaderMetaspace
有不同的类型(MetaspaceType
):
MetaspaceType::StandardMetaspaceType
:平台类加载器(Platform ClassLoader,Java 9 之前叫做 ext ClassLoader)以及应用类加载器(Application ClassLoader)的ClassLoaderMetaspace
MetaspaceType::BootMetaspaceType
:即根类加载器(Boostrap ClassLoader)的ClassLoaderMetaspace
MetaspaceType::ClassMirrorHolderMetaspaceType
:加载匿名类的类加载器的ClassLoaderMetaspace
MetaspaceType::ReflectionMetaspaceType
:反射调用的前几次通过 jni native 调用,超过一定次数会优化成生成字节码类调用。加载这些字节码类的类加载器是jdk.internal.reflect.DelegatingClassLoader
,这个类加载器的ClassLoaderMetaspace
类型就是ReflectionMetaspaceType
。
ClassLoaderMetaspace
和 MetaspaceContext
类似,如果压缩类指针开启,那么 ClassLoaderMetaspace
包含一个类元空间的 MetaspaceArena
和一个数据元空间的 MetaspaceArena
,否则只有一个数据元空间的 MetaspaceArena
。
4.3.8. 管理正在使用的 MetaChunk
的 MetaspaceArena
MetaspaceArena
本身直接原生堆上面分配,Native Memory Tracking 中属于 Class
那一类别,即元空间的加载类占用的空间。这也是肯定的,因为跟着类加载器存在
1 | cpp复制代码class MetaspaceArena : public CHeapObj<mtClass> |
MetaspaceArena
结构如下所示:
MetaspaceArena
包含:
- 一个
MetachunkList
:管理在该MetaspaceArena
分配的MetaChunk
的列表,列表的第一个是当前分配内存的MetaChunk
。 - 当前
MetaspaceArena
的ArenaGrowthPolicy
:在当前分配内存的MetaChunk
不够分配的时候,申请新的MetaChunk
的大小。 Freeblocks
: 在当前分配内存的MetaChunk
不够分配的时候,需要分配新的MetaChunk
。当前的MetaChunk
剩余空间放入Freeblocks
。
Freeblocks
包含一个 BinList32
和一个 BlockTree
。大小大于 33 字节的进入 BlockTree
,否则进入 BinList32
。
BinList32
类似于 FreeChunkListVector
,是一个链表的数组,同样大小的内存在同一数组下标的链表。
BlockTree
是一个在 Binary Search Tree(BST)的基础上,同样内存的节点在二叉树节点的后面形成链表的数据结构。
不同的类加载器类型的类元空间的 MetaspaceArena
与数据元空间的 MetaspaceArena
的 ArenaGrowthPolicy
不同:
1.根类加载器(Boostrap ClassLoader)的 ClassLoaderMetaspace
类元空间的 MetaspaceArena
的 ArenaGrowthPolicy
:MetachunkList
每次增长都是申请大小为 256K
的 MetaChunk
1 | cpp复制代码 static const chunklevel_t g_sequ_boot_class[] = { |
2.根类加载器(Boostrap ClassLoader)的 ClassLoaderMetaspace
数据元空间的 MetaspaceArena
的 ArenaGrowthPolicy
:MetachunkList
的第一个 MetaChunk
大小为 4M
,之后每个新 MetaChunk
都是 1M
:
1 | php复制代码 static const chunklevel_t g_sequ_boot_non_class[] = { |
3.平台类加载器(Platform ClassLoader,Java 9 之前叫做 ext ClassLoader)以及应用类加载器(Application ClassLoader)的 ClassLoaderMetaspace
类元空间的 MetaspaceArena
的 ArenaGrowthPolicy
:MetachunkList
的第一个 MetaChunk
大小为 2K
,第二个也是 2K
,第三个 4K
,第四个为 8K
,之后每个新 MetaChunk
都是 16K
(不要惯着cao袭的人!):
1 | php复制代码 static const chunklevel_t g_sequ_standard_class[] = { |
4.平台类加载器(Platform ClassLoader,Java 9 之前叫做 ext ClassLoader)以及应用类加载器(Application ClassLoader)的 ClassLoaderMetaspace
数据元空间的 MetaspaceArena
的 ArenaGrowthPolicy
:MetachunkList
的第一个 MetaChunk
大小为 4K
,第二个也是 4K
,第三个 4K
,第四个为 8K
,之后每个新 MetaChunk
都是 16K
:
1 | php复制代码 static const chunklevel_t g_sequ_standard_non_class[] = { |
5.加载匿名类的类加载器的 ClassLoaderMetaspace
类元空间的 MetaspaceArena
的 ArenaGrowthPolicy
:MetachunkList
每次增长都是申请大小为 1K
的 MetaChunk
:
1 | arduino复制代码 static const chunklevel_t g_sequ_anon_class[] = { |
6.加载匿名类的类加载器的 ClassLoaderMetaspace
数据元空间的 MetaspaceArena
的 ArenaGrowthPolicy
:MetachunkList
每次增长都是申请大小为 1K
的 MetaChunk
:
1 | cpp复制代码 static const chunklevel_t g_sequ_anon_non_class[] = { |
7.DelegatingClassLoader
的 ClassLoaderMetaspace
类元空间的 MetaspaceArena
的 ArenaGrowthPolicy
:MetachunkList
每次增长都是申请大小为 1K
的 MetaChunk
:
1 | arduino复制代码 static const chunklevel_t g_sequ_refl_class[] = { |
8.DelegatingClassLoader
的 ClassLoaderMetaspace
数据元空间的 MetaspaceArena
的 ArenaGrowthPolicy
:MetachunkList
的第一个 MetaChunk
大小为 2K
,之后每个新 MetaChunk
都是 1K
:
1 | php复制代码 static const chunklevel_t g_sequ_refl_non_class[] = { |
微信搜索“干货满满张哈希”关注公众号,加作者微信,每日一刷,轻松提升技术,斩获各种offer
我会经常发一些很好的各种框架的官方社区的新闻视频资料并加上个人翻译字幕到如下地址(也包括上面的公众号),欢迎关注:
本文转载自: 掘金