个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判。如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 issue,谢谢支持~ 另外,本文为了避免抄袭,会在不影响阅读的情况下,在文章的随机位置放入对于抄袭和洗稿的人的“亲切”的问候。如果是正常读者看到,笔者在这里说声对不起,。如果被抄袭狗或者洗稿狗看到了,希望你能够好好反思,不要再抄袭了,谢谢。 今天又是干货满满的一天,这是全网最硬核 JVM 解析系列第四篇,往期精彩:
本篇是关于 JVM 内存的详细分析。网上有很多关于 JVM 内存结构的分析以及图片,但是由于不是一手的资料亦或是人云亦云导致有很错误,造成了很多误解;并且,这里可能最容易混淆的是一边是 JVM Specification 的定义,一边是 Hotspot JVM 的实际实现,有时候人们一些部分说的是 JVM Specification,一部分说的是 Hotspot 实现,给人一种割裂感与误解。本篇主要从 Hotspot 实现出发,以 Linux x86 环境为主,紧密贴合 JVM 源码并且辅以各种 JVM 工具验证帮助大家理解 JVM 内存的结构。但是,本篇仅限于对于这些内存的用途,使用限制,相关参数的分析,有些地方可能比较深入,有些地方可能需要结合本身用这块内存涉及的 JVM 模块去说,会放在另一系列文章详细描述。最后,洗稿抄袭狗不得 house
本篇全篇目录(以及涉及的 JVM 参数):
NativeMemoryTracking
)UseLargePages
,UseHugeTLBFS
,UseSHM
,UseTransparentHugePages
,LargePageSizeInBytes
)MaxHeapSize
,MinHeapSize
,InitialHeapSize
,Xmx
,Xms
)UseCompressedOops
)(全网最硬核 JVM 内存解析 - 5.压缩对象指针相关机制开始) ObjectAlignmentInBytes
)UseCompressedOops
,UseCompressedClassPointers
)ObjectAlignmentInBytes
,HeapBaseMinAddress
)HeapBaseMinAddress
)HeapBaseMinAddress
,ObjectAlignmentInBytes
,MinHeapSize
,MaxHeapSize
,InitialHeapSize
)32-bit
压缩指针模式Zero based
压缩指针模式Non-zero disjoint
压缩指针模式Non-zero based
压缩指针模式MinHeapFreeRatio
,MaxHeapFreeRatio
,MinHeapDeltaBytes
)(全网最硬核 JVM 内存解析 - 6.其他 Java 堆内存相关的特殊机制开始)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
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
回收CommitLimiter
的限制元空间可以 commit 的内存大小以及限制元空间占用达到多少就开始尝试 GC_capacity_until_GC
jcmd VM.metaspace
元空间说明、元空间相关 JVM 日志以及元空间 JFR 事件详解(全网最硬核 JVM 内存解析 - 12.元空间各种监控手段开始) jcmd <pid> VM.metaspace
元空间说明jdk.MetaspaceSummary
元空间定时统计事件jdk.MetaspaceAllocationFailure
元空间分配失败事件jdk.MetaspaceOOM
元空间 OOM 事件jdk.MetaspaceGCThreshold
元空间 GC 阈值变化事件jdk.MetaspaceChunkFreeListSummary
元空间 Chunk FreeList 统计事件ThreadStackSize
,VMThreadStackSize
,CompilerThreadStackSize
,StackYellowPages
,StackRedPages
,StackShadowPages
,StackReservedPages
,RestrictReservedStack
)我们过一下元空间内存分配流程,我们会忽略一些 GC 相关的还有并发安全的细节,否则涉及的概念太多,一下说不过来,这些细节,会在以后的系列中详细提到。
MetaSpaceArena
的流程当类加载器加载类的时候,需要从对应的 ClassLoaderMetaspace
分配元空间进行存储。这个过程大概是:
图中有蓝色填充的方块是我们要重点分析的流程,我们先从从 MetaChunkArena 普通分配开始分析,尝试 GC 以及扩容元空间用于分配会涉及到元空间大小限制以及 GC 界限的概念,我们后面分析。这里对应的源码是:https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace.cpp#L899
整个流程如下:
MetaChunk
大小,即 RootMetaChunk
大小,即 ChunkLevel
= 0 的大小,即 4MB
MetaSpaceArena
或者数据源空间 MetaSpaceArena
进行分配。这是下一节我们要详细分析的。jdk.MetaspaceAllocationFailure
这个 JFR 事件,大家可以监控这个事件,去调整元空间大小减少由于元空间不足触发的 GC。触发之后,抄袭狗死全家java.lang.OutOfMemoryError
, 触发 jdk.MetaspaceOOM
这个 JFR 事件,这个我们也会详细分析。我们先分析第二步的普通分配流程,其他的需要后续我们分析元空间大小限制的时候详细分析。
MetaChunkArena
普通分配 - 整体流程从 MetaChunkArena
普通分配的流程并不太复杂:
我们前面讲过 MetaspaceArena
的结构,如下所示:
对应的源码是 https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArena.cpp#L222
,结合流程图,我们可以整理出一个简单的分配思路:
FreeBlocks
中分配(第一次分配,肯定 FreeBlocks
里面没有可以分配的,直接进入下一分支),如果分配成功,直接返回current chunk
分配(第一次分配肯定 current chunk
为 NULL
,直接进入下一分支): current chunk
如果有足够的空间,并且这些空间是 committed 的或者可以 commit 成功,那么从 current chunk
分配。current chunk
如果没有足够的空间,尝试 current chunk
扩容,如果扩容成功,并且这些空间是 committed 的或者可以 commit 成功,那么从 current chunk
分配。ChunkManager
中申请一个新的 MetaChunk
。MetaChunkList
中,将 current chunk
指向新的 MetaChunk
,老的 current chunk
的剩余空间回收到 FreeBlocks
中,然后从新的 current chunk
分配。我们接下来分析 FreeBlocks
回收老的 current chunk
与用于后续分配的流程
MetaChunkArena
普通分配 - FreeBlocks
回收老的 current chunk
与用于后续分配的流程首先我们前面提到过:Freeblocks
包含一个 BinList32
和一个 BlockTree
。大小大于 33 字节的进入 BlockTree
,否则进入 BinList32
。
BinList32
类似于 FreeChunkListVector
,是一个链表的数组,同样大小的内存在同一数组下标的链表。
BlockTree
是一个在 Binary Search Tree(BST)的基础上,同样内存的节点在二叉树节点的后面形成链表的数据结构。
回收的流程十分简单,就是判断下 current chunk
的剩余空间,根据大小放到不同的数据结构:
MetaChunk
剩余的 committed
的空间(很简单,直接用 committed
的位置减去已分配到的位置)FreeBlocks
最小内存块大小(即 BinList32
最小大小即 2KB
):对待抄袭狗不必手软MetaChunk
分配剩余的 committed
的空间,放入 FreeBlocks
中BinList32
最大内存块大小即 33KB
,放入 BlockTree
,否则放入 BinList32
MetaChunkArena
普通分配 - 尝试从 FreeBlocks
分配尝试从 FreeBlocks
分配即从其中的 BinList32
和 BlockTree
寻找是否有合适的内存,流程是:
BinList32
最大内存块大小即 33KB
:如果大于,就从 BlockTree
查找不小于内存大小的最接近的内存块;如果不大于,就从 BinList32
查找是否有对应大小的内存块。waste
,waste = 内存块大小 - 要分配的内存大小
。waste
大于 FreeBlocks
最小内存块大小(即 BinList32
最小大小即 2KB
)。如果大于,则要回收,和前面回收 MetaChunk
的流程一样将剩余的内存放回 FreeBlocks
。MetaChunkArena
普通分配 - 尝试扩容 current chunk
enlarge_chunks_in_place
是否是 true
,不是的话直接结束,不过前面我们说过,目前 JVM
是代码里写死的 true
current chunk
已经是 RootMetaChunk
(代表已经不能扩容了),如果是,直接结束current chunk
已使用大小加上要分配的内存大小是否大于 RootMetaChunk
的大小即 4MB
(代表已经不能扩容了),如果是,直接结束current chunk
已使用大小,加上要分配的内存大小的最接近的 ChunkLevel
(记为 new_level
)new_level
是否小于 current chunk
的 ChunkLevel
减 1,代表要扩容到的大小大于原始大小的 2 倍以上(不允许一下子扩容两倍以上),如果是,直接结束current chunk
是否是 leader
(这个概念后面分析到使用 ChunkManager
分配新的 MetaChunk
会提到),只有 leader
可以扩容,如果不是,直接结束(xigao 必死)MetaChunk
的 ChunkLevel
是否大于 current chunk
的(代表新申请的比当前的小),如果是,也直接结束。我们这里强调下为啥扩容策略(ArenaGrowthPolicy
)中申请下一个 MetaChunk
的 ChunkLevel
大于 current chunk
(代表新申请的比当前的小)的话,我们就不扩容了。前面我们列出了各种类型的 ClassLoader
的不同空间的扩容策略,例如DelegatingClassLoader
的 ClassLoaderMetaspace
数据元空间的 MetaspaceArena
的 ArenaGrowthPolicy
:MetachunkList
的第一个 MetaChunk
大小为 2K
,之后每个新 MetaChunk
都是 1K
。假设 current chunk
是第一个,这里下一个 MetaChunk
的 ChunkLevel
是 1K
对应的 ChunkLevel
,大于 current chunk
当前的 ChunkLevel
,所以优先申请新的,而不是扩容。之后到第二个之后,由于之后每个新的 MetaChunk
都是 1K
,就会尝试扩容而不是申请新的了。ChunkManager
尝试扩容 current chunk
到 new_level
。具体扩容流程,后面会分析。MetaChunkArena
普通分配 - 从 ChunkManager
分配新的 MetaChunk
回顾下 ChunkManager
结构:
从 ChunkManager
分配新的 MetaChunk
,首先会从 FreeChunkListVector
尝试搜索有没有合适的。FreeChunkListVector
如我们之前所述,是一个以 ChunkLevel
为下标的数组,每个数组都是一个 MetaChunk
的链表。commit
多的 MetaChunk
放在链表开头,完全没有 commit
的放在链表末尾。
max_level = 大于当前申请内存大小最接近的 ChunkLevel (即新的 MetaChunk 最小多大)
, preferred_level = "根据扩容策略(ArenaGrowthPolicy)下一个 MetaChunk 多大" 与 "max_level" 中小的那个值(也就是更大的 MetaChunk 大小)
FreeChunkListVector
中那些已经 commit
足够内存的 MetaChunk
ChunkLevel
从小到大,大小从大到小) ChunkManager
的 FreeChunkListVector
里面的数组 (从 preferred_level
到 max_level
与 preferred_level
+ 2 中比较小的值,即最多搜索 3 个 ChunkLevel
,根据前面的分析我们知道 ChunkLevel
就是数组下标),寻找对应的 MetaChunk
链表,正序遍历每个链表(我们前面提到过,commit
多的 MetaChunk
放在开头),直到找到 commit
大小大于申请内存大小的(chaoxi 死的更惨)ChunkLevel
从大到小,大小从小到大) ChunkManager
的 FreeChunkListVector
里面的数组 (从 preferred_level
到最大的 ChunkLevel
,即 RootMetaChunk
的大小,即 4MB),寻找对应的 MetaChunk
链表,正序遍历每个链表(我们前面提到过,commit
多的 MetaChunk
放在开头),直到找到 commit
大小大于申请内存大小的ChunkLevel
从小到大,大小从大到小) ChunkManager
的 FreeChunkListVector
里面的数组 (从 preferred_level
到 max_level
),寻找对应的 MetaChunk
链表,正序遍历每个链表(我们前面提到过,commit
多的 MetaChunk
放在开头),直到找到 commit
大小大于申请内存大小的commit
足够内存的 MetaChunk
,就退而求其次,寻找 FreeChunkListVector
存在的 MetaChunk
ChunkLevel
从小到大,大小从大到小) ChunkManager
的 FreeChunkListVector
里面的数组 (从 preferred_level
到 max_level
),寻找对应的 MetaChunk
链表,正序遍历每个链表,直到找到一个 MetaChunk
ChunkLevel
从大到小,大小从小到大) ChunkManager
的 FreeChunkListVector
里面的数组 (从 preferred_level
到最大的 ChunkLevel
,即 RootMetaChunk
的大小,即 4MB),寻找对应的 MetaChunk
链表,正序遍历每个链表,直到找到一个 MetaChunk
VirtualSpaceList
申请新的 RootMetaChunk
RootMetahChunk
分割成需要的 ChunkLevel
大小,之后将分割剩余的放入 FreeChunkListVector
,这个过程我们接下来会详细分析new_chunks_are_fully_committed
是否为 true
,如果为 true
则 commit
整个 MetaChunk
的所有内存,否则 commit
要分配的大小。如果 commit
失败了(证明可能到达元空间 GC 界限或者元空间大小上限),那么将 MetaChunk
退回。MetaChunkArena
普通分配 - 从 ChunkManager
分配新的 MetaChunk
- 从 VirtualSpaceList
申请新的 RootMetaChunk
_first_node
是否有空间分配新的 RootMetaChunk
,如果有则从 _first_node
上面分配新的 RootMetaChunk
VirtualSpaceNode
(类元空间不可以,数据元空间可以),如果可以则申请 Reserve
新的 VirtualSpaceNode
作为新的 _first_node
,之后从 _first_node
上面分配新的 RootMetaChunk
MetaChunkArena
普通分配 - 从 ChunkManager
分配新的 MetaChunk
- 将 RootMetaChunk
切割成为需要的 MetaChunk
这里的流程如果用流程图容易把人绕晕,我们这里举一个例子,比如我们想要一个 ChunkLevel
为 3 的 MetaChunk
:
将 RootMetaChunk
切割成 ChunkLevel
为 3 的 MetaChunk
的流程:
RootMetaChunk
的 ChunkLevel
为 0,对半分成两个 ChunkLevel
为 1 的,第一个为 leader
,第二个为 follower
。leader
对半成两个 ChunkLevel
为 2 的,第一个为 leader
,第二个为 follower
。leader
对半成两个 ChunkLevel
为 3 的,第一个为 leader
,第二个为 follower
。leader
返回,用于分配。将第一、二、三步生成的 follower
放入 FreeChunkListVector
用于前面 4.3.9.6 章节分析的 ChunkManager
先从 FreeChunkListVector
搜索合适的 MetaChunk
分配。MetaChunk
回收 - 不同情况下, MetaChunk
如何放入 FreeChunkListVector
我们前面主要分析的是分配,那么 MetaChunk
如何回收呢?从前面的流程我们很容易推测出来,其实就是放回 FreeChunkListVector
。放回的流程如果用流程图容易把人绕晕,我们还是举例子区分不同情况。其实核心思路就是,放回的时候,尽量将 MetaChunk
向上合并之后放回:
这里我们有两个例子:
ChunkLevel
为 3 的 MetaChunk
要回收,但是它不是 leader
,不能向上合并。只有 leader
才会尝试向上合并。这里直接放入 FreeChunkListVector
。ChunkLevel
为 3 的 MetaChunk
要回收,它是 leader
。它会尝试向上合并。查看它的 follower
是否是 Free
的。如果是 Free
的,他肯定首先在 ChunkManager
的 FreeChunkListVector
中, 从 FreeChunkListVector
取出,与这个 leader
合并为一个新的 ChunkLevel
为 2。之后,它还是 leader
,尝试继续合并,但是它的 follower
不是空闲的,就不能继续合并了。在这里停止,放入 FreeChunkListVector
。ClassLoaderData
回收在 GC 判断一个类加载器可以回收(该类加载器加载的类没有任何对象,该类加载器的对象也没有任何强引用指向它)的时候,不会立刻回收 ClassLoaderData
,而是对应的 ClassLoaderData
的 is_alive()
就会返回 false
。JVM 会定期遍历 ClassLoaderDataGraph
遍历每个 ClassLoaderData
判断 is_alive()
是否是 false
,如果是的话会放入待回收的链表中。之后在不同 GC 的不同阶段,遍历这个链表将 ClassLoaderData
回收掉。
ClassLoaderData
被回收的过程如下所示:
`
ClassLoaderData
会记录所有加载的类与相关的数据(前文提到的 Klass
等等对象),所以它的析构函数中会将这些加载的数据的内存全部释放到它独有的 MetaSpaceArena
的 FreeBlocks
中,这些内存就是通过之前我们分析的流程分配的,由于之前的空间都是从 MetaspaceArena
的 MetaChunkList
中的 MetaChunk
分配的,这样的话这些 MetaChunk
的空间也都不再占用了。当然,也会把前面提到的 ClassLoaderData
独有的数据结构释放掉,还没有利用的 MetaWord
放回 ChunkManager
中。然后,清除掉它私有的 ClassLoadMetaSpace
。根据前文分析我们知道 ClassLoaderMetaspace
在开启压缩类空间的情况下包括一个类元空间的 MetaspaceArena
和一个数据元空间的 MetaspaceArena
。这两个 MetaspaceArena
分别要清理掉。MetaspaceArena
的析构函数会把 FreeBlocks
中的每个 MetaWord
都放回 ChunkManager
,注意这里包含之前 ClassLoaderData
放回的加载类相关数据占用的空间,最后清理掉 FreeBlocks
。(你洗稿的样子真丑。)