个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判。如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 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
)前文我们没有提到,如何限制元空间的大小,其实就是限制 commit
的内存大小。元空间的限制不只是受限于我们的参数配置,并且前面我们提到了,元空间的内存回收也比较特殊,元空间的内存基本都是每个类加载器的 ClassLoaderData
申请并管理的,在类加载器被 GC 回收后,ClassLoaderData
管理的这些元空间也会被回收掉。所以,GC 是可能触发一部分元空间被回收了。所以元空间在设计的时候,还有一个动态限制 _capacity_until_GC
,即触发 GC 的元空间占用大小。当要分配的空间导致元空间整体占用超过这个限制的时候,尝试触发 GC。这个动态限制也会在每次 GC 的时候动态扩大或者缩小。动态扩大以及缩小
我们先回顾下之前提过的参数配置:
MetaspaceSize
:初始元空间大小,也是最小元空间大小。后面元空间大小伸缩的时候,不会小于这个大小。默认是 21M。MaxMetaspaceSize
:最大元空间大小,默认是无符号 int 最大值。MinMetaspaceExpansion
:每次元空间大小伸缩的时候,至少改变的大小。默认是 256K。MaxMetaspaceExpansion
:每次元空间大小伸缩的时候,最多改变的大小。默认是 4M。MaxMetaspaceFreeRatio
:最大元空间空闲比例,默认是 70,即 70%。MinMetaspaceFreeRatio
:最小元空间空闲比例,默认是 40,即 40%。CommitLimiter
的限制元空间可以 commit 的内存大小以及限制元空间占用达到多少就开始尝试 GCCommitLimiter
是一个全局单例,用来限制元空间可以 commit
的内存大小。每次分配元空间 commit
内存的时候,都会调用 CommitLimiter::possible_expansion_words
方法,这个方法会检查:
commit
的内存大小加上要分配的大小是否超过了 MaxMetaspaceSize
commit
的内存大小加上要分配的大小是否超过了 _capacity_until_GC
,超过了就尝试触发 GC尝试 GC 的核心逻辑是:
GCLocker
是否锁定禁止 GC,如果是的话,首先尝试提高 _capacity_until_GC
进行分配,分配成功直接返回,否则需要阻塞等待 GCLocker
释放QueuedAllocationWarningCount
,如果尝试触发 GC 的次数超过这个次数,就会打印一条警告日志,当然 QueuedAllocationWarningCount
默认是 0,不会打印,并且触发多次 GC 也无法满足的概率比较低)_capacity_until_GC
在 JVM 初始化的时候,_capacity_until_GC
先会设置为 MaxMetaspaceSize
,因为 JVM 初始化的时候会加载很多类,并且这时候要避免触发 GC。在初始化之后,将 _capacity_until_GC
设置为当前元空间占用大小与 MetaspaceSize
中比较大的那个值。同时,还会初始化一个 _shrink_factor
,这个 _shrink_factor
主要是如果需要缩小元空间大小,每次缩小的比例。洗稿的狗也遇到不少
之后,在每次 GC 回收之后,需要重新计算新的 _capacity_until_GC
:
crrent_shrink_factor = _shrink_factor
,统计当前元空间使用的空间 used_after_gc
。MinMetaspaceFreeRatio
最小元空间空闲比例计算 minimum_free_percentage
和 maximum_used_percentage
,看是否需要扩容。minimum_desired_capacity
:使用当前元空间使用的空间 used_after_gc
除以 maximum_used_percentage
,并且保证它不小于初始元空间大小 MetaspaceSize
,不大于最大元空间大小 MaxMetaspaceSize
。_capacity_until_GC
小于计算的当前元空间至少要多大 minimum_desired_capacity
,那么就查要扩容的空间是否大于等于配置 MinMetaspaceExpansion
,以及小于等于 MaxMetaspaceExpansion
,只有满足才会真正扩容。_capacity_until_GC
MaxMetaspaceFreeRatio
最大元空间空闲比例计算 minimum_free_percentage
和 maximum_used_percentage
,看是否需要缩容。maximum_desired_capacity
:使用当前元空间使用的空间 used_after_gc
除以 maximum_used_percentage
,并且保证它不小于初始元空间大小 MetaspaceSize
,不大于最大元空间大小 MaxMetaspaceSize
。_capacity_until_GC
大于计算的当前元空间至少要多大 maximum_desired_capacity
,计算 shrink_bytes
= _capacity_until_GC
减去 maximum_desired_capacity
。_shrink_factor
初始为 0,之后为 10%,之后每次翻 4 倍,直到 100%。扩容的大小为 shrink_bytes
乘以这个百分比MinMetaspaceExpansion
,以及小于等于 MaxMetaspaceExpansion
,并且缩容后不会小于初始元空间大小 MetaspaceSize
,就会缩容。_capacity_until_GC
我们还可以看出,如果我们设置 MinMetaspaceFreeRatio
为 0,那么就不会扩容,如果设置 MaxMetaspaceFreeRatio
为 100,那么就不会缩容。_capacity_until_GC
就不会因为 GC 更改。