https://cloud.tencent.com/developer/article/2277323
个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判。如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 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 参数:
3. Java 堆内存相关设计
3.1. 通用初始化与扩展流程
目前最新的 JVM,主要根据三个指标初始化堆以及扩展或缩小堆:
- 最大堆大小
- 最小堆大小
- 初始堆大小
不同的 GC 情况下,初始化以及扩展的流程可能在某些细节不太一样,但是,大体的思路都是:
- 初始化阶段,reserve 最大堆大小,并且 commit 初始堆大小
- 在某些 GC 的某些阶段,根据上次 GC 的数据,动态扩展或者缩小堆大小,扩展就是 commit 更多,缩小就是 uncommit 一部分内存。但是,堆大小不会小于最小堆大小,也不会大于最大堆大小
3.2. 直接指定三个指标(MinHeapSize,MaxHeapSize,InitialHeapSize)的方式
这三个指标,直接对应的 JVM 参数是:
- 最大堆大小:
MaxHeapSize
,如果没有指定的话会有默认预设值用于指导 JVM 计算这些指标的大小,下一章节会详细分析,预设值为 125MB 左右(96M*13/10) - 最小堆大小:
MinHeapSize
,默认为 0,0 代表让 JVM 自己计算,下一章节会详细分析 - 初始堆大小:
InitialHeapSize
,默认为 0,0 代表让 JVM 自己计算,下一章节会详细分析
对应源码是:https://github.com/openjdk/jdk/blob/jdk-21+3/src/hotspot/share/gc/shared/gc_globals.hpp
:
#define ScaleForWordSize(x) align_down((x) * 13 / 10, HeapWordSize)product(size_t, MaxHeapSize, ScaleForWordSize(96*M), \"Maximum heap size (in bytes)") \constraint(MaxHeapSizeConstraintFunc,AfterErgo) \
product(size_t, MinHeapSize, 0, \"Minimum heap size (in bytes); zero means use ergonomics") \constraint(MinHeapSizeConstraintFunc,AfterErgo) \
product(size_t, InitialHeapSize, 0, \"Initial heap size (in bytes); zero means use ergonomics") \constraint(InitialHeapSizeConstraintFunc,AfterErgo) \
我们可以通过类似于 -XX:MaxHeapSize=1G
这种启动参数对这三个指标进行设置,但是,我们经常看到的可能是 Xmx
以及 Xms
这两个参数设置这三个指标,这两个参数分别对应:
Xmx
:对应 最大堆大小 等价于MaxHeapSize
Xms
:相当于同时设置最小堆大小MinHeapSize
和初始堆大小InitialHeapSize
对应的 JVM 源码是:https://github.com/openjdk/jdk/blob/jdk-21+3/src/hotspot/share/runtime/arguments.cpp
:
//如果设置了 Xms
else if (match_option(option, "-Xms", &tail)) {julong size = 0;//解析 Xms 大小ArgsRange errcode = parse_memory_size(tail, &size, 0);if (errcode != arg_in_range) {jio_fprintf(defaultStream::error_stream(),"Invalid initial heap size: %s\n", option->optionString);describe_range_error(errcode);return JNI_EINVAL;}//将解析的值设置到 MinHeapSizeif (FLAG_SET_CMDLINE(MinHeapSize, (size_t)size) != JVMFlag::SUCCESS) {return JNI_EINVAL;}//将解析的值设置到 InitialHeapSizeif (FLAG_SET_CMDLINE(InitialHeapSize, (size_t)size) != JVMFlag::SUCCESS) {return JNI_EINVAL;}
//如果设置了 Xmx
} else if (match_option(option, "-Xmx", &tail) || match_option(option, "-XX:MaxHeapSize=", &tail)) {julong long_max_heap_size = 0;//解析 Xmx 大小ArgsRange errcode = parse_memory_size(tail, &long_max_heap_size, 1);if (errcode != arg_in_range) {jio_fprintf(defaultStream::error_stream(),"Invalid maximum heap size: %s\n", option->optionString);describe_range_error(errcode);return JNI_EINVAL;}//将解析的值设置到 MaxHeapSizeif (FLAG_SET_CMDLINE(MaxHeapSize, (size_t)long_max_heap_size) != JVMFlag::SUCCESS) {return JNI_EINVAL;}
}
最后提一句,JVM 启动参数,同一个参数可以多次出现,但是只有最后一个会生效,例如:
java -XX:MaxHeapSize=8G -XX:MaxHeapSize=4G -XX:MaxHeapSize=8M -version
这个命令启动的 JVM MaxHeapSize 为 8MB。由于前面提到 Xmx 与 MaxHeapSize 是等价的,所以这么写也是可以的(虽然最后 MaxHeapSize 还是 8MB):
java -Xmx=8G -XX:MaxHeapSize=4G -XX:MaxHeapSize=8M -version
3.3. 不手动指定三个指标的情况下,这三个指标(MinHeapSize,MaxHeapSize,InitialHeapSize)是如何计算的
上一章节我们提到我们可以手动指定这三个参数,如果不指定呢?JVM 会怎么计算这三个指标的大小?首先,当然,JVM 会读取 JVM 可用内存:首先 JVM 需要知道自己可用多少内存,我们称为可用内存。由此引入第一个 JVM 参数,MaxRAM
,这个参数是用来明确指定 JVM 进程可用内存大小的,如果没有指定,JVM 会自己读取系统可用内存。这个可用内存用来指导 JVM 限制最大堆内存。后面我们会看到很多 JVM 参数与这个可用内存相关。
前面我们还提到了,就算没有指定 MaxHeapSize
或者 Xmx
,MaxHeapSize
也有自己预设的一个参考值。源码中这个预设参考值为 125MB 左右(96M*13/10
)。但是一般最后不会以这个参考值为准,JVM 初始化的时候会有很复杂的计算计算出合适的值。比如你可以在你的电脑上执行下下面的命令,可以看到类似下面的输出:
> java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal -version|grep MaxHeapSizesize_t MaxHeapSize = 1572864000 {product} {ergonomic}size_t SoftMaxHeapSize = 1572864000 {manageable} {ergonomic}
openjdk version "17.0.2" 2022-01-18 LTS
OpenJDK Runtime Environment Corretto-17.0.2.8.1 (build 17.0.2+8-LTS)
OpenJDK 64-Bit Server VM Corretto-17.0.2.8.1 (build 17.0.2+8-LTS, mixed mode, sharing)
可以看到 MaxHeapSize
的大小,以及它的值是通过 ergonomic 决定的。也就是非人工指定而是 JVM 自己算出来的。
上面提到的那个 125MB 左右的初始参考值,一般用于 JVM 计算。我们接下来就会分析这个计算流程,首先是最大堆内存 MaxHeapSize 的计算流程:
流程中涉及了以下几个参数,还有一些已经过期的参数,会被转换成未过期的参数:
MinRAMPercentage
:注意不要被名字迷惑,这个参数是在可用内存比较小的时候生效,即最大堆内存占用为可用内存的这个参数指定的百分比,默认为 50,即 50%MaxRAMPercentage
:注意不要被名字迷惑,这个参数是在可用内存比较大的时候生效,即最大堆内存占用为可用内存的这个参数指定的百分比,默认为 25,即 25%ErgoHeapSizeLimit
:通过自动计算,计算出的最大堆内存大小不超过这个参数指定的大小,默认为 0 即不限制MinRAMFraction
: 已过期,如果配置了会转化为MinRAMPercentage
换算关系是:MinRAMPercentage
= 100.0 /MinRAMFraction
,默认是 2MaxRAMFraction
: 已过期,如果配置了会转化为MaxRAMPercentage
换算关系是:MaxRAMPercentage
= 100.0 /MaxRAMFraction
,默认是 4
对应的源码是:https://github.com/openjdk/jdk/blob/jdk-21+3/src/hotspot/share/gc/shared/gc_globals.hpp
:
product(double, MinRAMPercentage, 50.0, \"Minimum percentage of real memory used for maximum heap" \"size on systems with small physical memory size") \range(0.0, 100.0) \
product(double, MaxRAMPercentage, 25.0, \"Maximum percentage of real memory used for maximum heap size") \range(0.0, 100.0) \
product(size_t, ErgoHeapSizeLimit, 0, \"Maximum ergonomically set heap size (in bytes); zero means use " \"MaxRAM * MaxRAMPercentage / 100") \range(0, max_uintx) \
product(uintx, MinRAMFraction, 2, \"Minimum fraction (1/n) of real memory used for maximum heap " \"size on systems with small physical memory size. " \"Deprecated, use MinRAMPercentage instead") \range(1, max_uintx) \
product(uintx, MaxRAMFraction, 4, \"Maximum fraction (1/n) of real memory used for maximum heap " \"size. " \"Deprecated, use MaxRAMPercentage instead") \range(1, max_uintx) \
然后如果我们也没有设置 MinHeapSize
以及 InitialHeapSize
,也会经过下面的计算过程计算出来:
流程中涉及了以下几个参数,还有一些已经过期的参数,会被转换成未过期的参数:
NewSize
:初始新生代大小,预设值为 1.3MB 左右(1*13/10
)OldSize
:老年代大小,预设值为 5.2 MB 左右(4*13/10
)InitialRAMPercentage
:初始堆内存为可用内存的这个参数指定的百分比,默认为 1.5625,即 1.5625%InitialRAMFraction
: 已过期,如果配置了会转化为InitialRAMPercentage
换算关系是:InitialRAMPercentage
= 100.0 /InitialRAMFraction
对应的源码是:https://github.com/openjdk/jdk/blob/jdk-21+3/src/hotspot/share/gc/shared/gc_globals.hpp
:
product(size_t, NewSize, ScaleForWordSize(1*M), \"Initial new generation size (in bytes)") \constraint(NewSizeConstraintFunc,AfterErgo) \
product(size_t, OldSize, ScaleForWordSize(4*M), \"Initial tenured generation size (in bytes)") \range(0, max_uintx) \
product(double, InitialRAMPercentage, 1.5625, \"Percentage of real memory used for initial heap size") \range(0.0, 100.0) \
product(uintx, InitialRAMFraction, 64, \"Fraction (1/n) of real memory used for initial heap size. " \"Deprecated, use InitialRAMPercentage instead") \range(1, max_uintx) \