个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判。如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 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
)目前最新的 JVM,主要根据三个指标初始化堆以及扩展或缩小堆:
不同的 GC 情况下,初始化以及扩展的流程可能在某些细节不太一样,但是,大体的思路都是:
这三个指标,直接对应的 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;
}
//将解析的值设置到 MinHeapSize
if (FLAG_SET_CMDLINE(MinHeapSize, (size_t)size) != JVMFlag::SUCCESS) {
return JNI_EINVAL;
}
//将解析的值设置到 InitialHeapSize
if (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;
}
//将解析的值设置到 MaxHeapSize
if (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
上一章节我们提到我们可以手动指定这三个参数,如果不指定呢?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 MaxHeapSize
size_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) \