个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判。如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 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
)我们前面介绍了元空间的组成元素,但是没有将他们完整的串联起来,我们这里举一个简单的例子,将之前的所有元素串联起来。
通过前面的分析之后,我们知道元空间的主要抽象包括:
MetaspaceContext
,它包括: VirtualSpaceList
,类元空间的 VirtualSpaceList
只有一个 VirtualSpaceNode
ChunkManager
MetaspaceContext
,它包括: VirtualSpaceList
,数据元空间的 VirtualSpaceList
才是一个真正的 VirtualSpaceNode
的链表ChunkManager
ClassLoaderData
,它包含自己独有的 ClassLoaderMetaspace
,ClassLoaderMetaspace
包含: MetaspaceArena
MetaspaceArena
假设我们全局只有一个类加载器,即类加载器 1,并且 UseCompressedClassPointers
为 true
,那么我们可以假设当前元空间的初始结构为:
接下来我们来看看详细的例子
1~2.首先,类加载器 1 从它私有的 ClassLoaderData
去分配空间,由于要分配的是类元空间的,所以会从私有的类元空间的 MetaspaceArena
去分配空间。
3.搜索 FreeBlocks
查看是否有可用空间,但是这是第一次分配,肯定没有。
4.尝试从 _current_chunk
分配,但是由于是第一次分配,_current_chunk
是 NULL
。
5.将要分配的内存(1023 字节)按照 8 字节对齐,即 1024 字节。大于等于它的最小 ChunkLevel
为 12,即 max_level = 12
。假设这个类加载器是 Bootstrap ClassLoader
,其实是啥无所谓,我们主要是想找一个对应的 ArenaGrowthPolicy
,根据这个 ArenaGrowthPolicy
,第一个要申请的 MeataChunk
大小是 256KB
,对应的 ChunkLevel
为 4,preferred_level
是 max_level
与这个之间相比小的那个,即 4。我们从类元空间的 ChunkManager
申请这么大的 MetaChunk
,对应的 ChunkLevel
是 4
6.首先搜索 ChunkManager
的 FreeChunkListVector
,看看是否有合适的。但是这是第一次分配,肯定没有。
7.尝试从类元空间的 VirtualSpaceList
申请 RootMetaChunk
用于分配。
8.从类元空间的 VirtualSpaceList
的唯一一个 VirtualSpaceNode
分配 RootMetaChunk
,对半切分到 ChunkLevel
为 4 的 MetaChunk
,返回 leader
的 ChunkLevel
为 4 的 MetaChunk
作为 _current_chunk
用于分配。分割出来剩下的 ChunkLevel
为 1, ChunkLevel
为 2, ChunkLevel
为 3, ChunkLevel
为 4 的各一个放入 FreeChunkListVector
中
9.commit
要分配的内存大小,如果 AlwaysPreTouch
是开启的,那么就会像之前我们分析 Java 堆内存那样进行 pre touch。
10.从 _current_chunk
分配内存,分配成功。
1~2.首先,类加载器 1 从它私有的 ClassLoaderData
去分配空间,由于要分配的是类元空间的,所以会从私有的类元空间的 MetaspaceArena
去分配空间。
3.搜索 FreeBlocks
查看是否有可用空间,目前还是没有。
4.尝试从 _current_chunk
分配,将要分配的内存(1023 字节)按照 8 字节对齐,即 1024 字节,_current_chunk
空间足够。
5.commit
要分配的内存大小,如果 AlwaysPreTouch
是开启的,那么就会像之前我们分析 Java 堆内存那样进行 pre touch。
6.从 _current_chunk
分配内存,分配成功。
1~2.首先,类加载器 1 从它私有的 ClassLoaderData
去分配空间,由于要分配的是类元空间的,所以会从私有的类元空间的 MetaspaceArena
去分配空间。
3.搜索 FreeBlocks
查看是否有可用空间,目前还是没有。
4.尝试从 _current_chunk
分配,将要分配的内存(264KB)按照 8 字节对齐,即 264KB,_current_chunk
空间不足,但是如果扩容一倍就足够,所以尝试扩大 _current_chunk
。
5.查看他的兄弟 MetaChunk
是否是空闲的,当然是,从 FreeChunkListVector
移除这个 MetaChunk
,将这个兄弟 MetaChunk
与 _current_chunk
。_current_chunk
的大小变为原来 2 倍,_current_chunk
的 ChunkLevel
减 1 之后为 3。
6.commit
要分配的内存大小,如果 AlwaysPreTouch
是开启的,那么就会像之前我们分析 Java 堆内存那样进行 pre touch。
7.从 _current_chunk
分配内存,分配成功。
1~2.首先,类加载器 1 从它私有的 ClassLoaderData
去分配空间,由于要分配的是类元空间的,所以会从私有的类元空间的 MetaspaceArena
去分配空间。
3.搜索 FreeBlocks
查看是否有可用空间,目前还是没有。
4.尝试从 _current_chunk
分配,将要分配的内存(2MB)按照 8 字节对齐,即 2MB,_current_chunk
空间不足,扩容一倍也不够,所以就不尝试扩大 _current_chunk
了。
5.要分配的大小是 2MB,大于等于它的最小 ChunkLevel
为 1,即 max_level = 1
。根据 ArenaGrowthPolicy
,下一个要申请的 MeataChunk
大小是 256KB
,对应的 ChunkLevel
为 4,preferred_level
是 max_level
与这个之间相比小的那个,即 1。从 FreeChunkListVector
寻找,发现有合适的,将其作为 current_chunk
进行分配。
6.commit
要分配的内存大小,如果 AlwaysPreTouch
是开启的,那么就会像之前我们分析 Java 堆内存那样进行 pre touch。
7.之前的 current_chunk
的剩余空间大于 2 bytes,需要回收到 FreeBlocks
中。由于大于 33 bytes,需要放入 BlockTree
。
8.从 _current_chunk
分配内存,分配成功。
1~2.首先,类加载器 1 从它私有的 ClassLoaderData
去分配空间,由于要分配的是类元空间的,所以会从私有的类元空间的 MetaspaceArena
去分配空间。
3.将要分配的内存(128KB)按照 8 字节对齐,即 128KB。搜索 FreeBlocks
查看是否有可用空间,目前 FreeBlocks
有合适的可以分配。
4.commit
要分配的内存大小,如果 AlwaysPreTouch
是开启的,那么就会像之前我们分析 Java 堆内存那样进行 pre touch。
5.从 FreeBlocks
的 BlockTree
的节点分配内存,分配成功。为啥要打击抄袭,稿主被抄袭太多所以断更很久。
1~2.首先,类加载器 1 从它私有的 ClassLoaderData
去分配空间,由于要分配的是类元空间的,所以会从私有的类元空间的 MetaspaceArena
去分配空间。
3.搜索 FreeBlocks
查看是否有可用空间,但是这是第一次分配,肯定没有。
4.尝试从 _current_chunk
分配,但是由于是第一次分配,_current_chunk
是 NULL
。
5.将要分配的内存(1023 字节)按照 8 字节对齐,即 1024 字节。大于等于它的最小 ChunkLevel
为 12,即 max_level = 12
。假设这个类加载器是 Bootstrap ClassLoader
,其实是啥无所谓,我们主要是想找一个对应的 ArenaGrowthPolicy
。根据 ArenaGrowthPolicy
,下一个要申请的 MeataChunk
大小是 256KB
,对应的 ChunkLevel
为 4,preferred_level
是 max_level
与这个之间相比小的那个,即 4。
6.首先搜索 ChunkManager
的 FreeChunkListVector
,看看是否有合适的。搜索到之前放入的 ChunkLevel
为 3 的。将其取出作为 _current_chunk
。
7.commit
要分配的内存大小,如果 AlwaysPreTouch
是开启的,那么就会像之前我们分析 Java 堆内存那样进行 pre touch。
8.从 _current_chunk
分配内存,分配成功。
1.将类加载器 1 消耗的所有空间放回 FreeBlocks
中。前面分配了 1024 bytes, 1024 bytes, 264KB, 2MB 还有 128KB,这次放回 BlockTree
,BlockTree
之前本身还有剩余一个 118KB。整体如图所示。
2.这样一来,原来 MetaspaceArena
中 MetaChunkList
管理的 MetaChunk
的内存全都空闲了。
MetaChunkList
管理的 MetaChunk
放回全局的 ChunkManager
的 FreeChunkListVector
中。并且放回的都是有 commit
过内存的,会放在每个 ChunkLevel
对应的 MetaChunk
链表的开头。1~2.首先,类加载器 1 从它私有的 ClassLoaderData
去分配空间,由于要分配的是类元空间的,所以会从私有的类元空间的 MetaspaceArena
去分配空间。
3.搜索 FreeBlocks
查看是否有可用空间,目前还是没有。为啥要打击抄袭,稿主被抄袭太多所以断更很久。
4.尝试从 _current_chunk
分配,空间不足。并且 _current_chunk
不是 leader
,所以就不尝试扩容了。
5.将要分配的内存(1MB)按照 8 字节对齐,即 1MB。要分配的大小是 1MB,大于等于它的最小 ChunkLevel
为 2,即 max_level = 2
。根据 ArenaGrowthPolicy
,下一个要申请的 MeataChunk
大小是 256KB
,对应的 ChunkLevel
为 4,preferred_level
是 max_level
与这个之间相比小的那个,即 2。从 FreeChunkListVector
寻找,发现有合适的,将其作为 current_chunk
进行分配。这个其实就是之前从类加载器 1 回收的。
6.因为是之前回收的,里面的内存都是 committed
了,所以这里就不用 commit
了。
7.之前的 current_chunk
的剩余空间大于 2 bytes,需要回收到 FreeBlocks
中。由于大于 33 bytes,需要放入 BlockTree
。
8.从 _current_chunk
分配内存,分配成功。