个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判。如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 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 下内存管理模型简述
- 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.元空间存储的元数据开始)
- 什么是元数据,为什么需要元数据
- 什么时候用到元空间,元空间保存什么
- 什么时候用到元空间,以及释放时机
- 元空间保存什么
- 元空间的核心概念与设计(全网最硬核 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. JVM 元空间设计
4.5. 元空间大小限制与动态伸缩
前文我们没有提到,如何限制元空间的大小,其实就是限制 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%。
4.5.1. CommitLimiter
的限制元空间可以 commit 的内存大小以及限制元空间占用达到多少就开始尝试 GC
CommitLimiter
是一个全局单例,用来限制元空间可以 commit
的内存大小。每次分配元空间 commit
内存的时候,都会调用 CommitLimiter::possible_expansion_words
方法,这个方法会检查:
- 当前元空间已经
commit
的内存大小加上要分配的大小是否超过了MaxMetaspaceSize
- 当前元空间已经
commit
的内存大小加上要分配的大小是否超过了_capacity_until_GC
,超过了就尝试触发 GC
尝试 GC 的核心逻辑是:
- 重新尝试分配
- 如果还是分配失败,检查
GCLocker
是否锁定禁止 GC,如果是的话,首先尝试提高_capacity_until_GC
进行分配,分配成功直接返回,否则需要阻塞等待GCLocker
释放 - 如果没有锁定,尝试触发 GC,之后回到第 1 步 (这里有个小参数
QueuedAllocationWarningCount
,如果尝试触发 GC 的次数超过这个次数,就会打印一条警告日志,当然QueuedAllocationWarningCount
默认是 0,不会打印,并且触发多次 GC 也无法满足的概率比较低)
4.5.2. 每次 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 更改。