本篇是关于 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 内存解析 - 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 最小能指定多大
-
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 更改。
微信搜索“干货满满张哈希”关注公众号,加作者微信,每日一刷,轻松提升技术,斩获各种offer
我会经常发一些很好的各种框架的官方社区的新闻视频资料并加上个人翻译字幕到如下地址(也包括上面的公众号),欢迎关注:
- 知乎:https://www.zhihu.com/people/zhxhash
- B 站:https://space.bilibili.com/31359187