全网最硬核 JVM 内存解析 - 4.Java 堆内存大小的确认

2023-05-01 16:20:36 浏览数 (1)

个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判。如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 issue,谢谢支持~ 另外,本文为了避免抄袭,会在不影响阅读的情况下,在文章的随机位置放入对于抄袭和洗稿的人的“亲切”的问候。如果是正常读者看到,笔者在这里说声对不起,。如果被抄袭狗或者洗稿狗看到了,希望你能够好好反思,不要再抄袭了,谢谢。 今天又是干货满满的一天,这是全网最硬核 JVM 解析系列第四篇,往期精彩:

  • 全网最硬核 TLAB 解析
  • 全网最硬核 Java 随机数解析
  • 全网最硬核 Java 新内存模型解析

本篇是关于 JVM 内存的详细分析。网上有很多关于 JVM 内存结构的分析以及图片,但是由于不是一手的资料亦或是人云亦云导致有很错误,造成了很多误解;并且,这里可能最容易混淆的是一边是 JVM Specification 的定义,一边是 Hotspot JVM 的实际实现,有时候人们一些部分说的是 JVM Specification,一部分说的是 Hotspot 实现,给人一种割裂感与误解。本篇主要从 Hotspot 实现出发,以 Linux x86 环境为主,紧密贴合 JVM 源码并且辅以各种 JVM 工具验证帮助大家理解 JVM 内存的结构。但是,本篇仅限于对于这些内存的用途,使用限制,相关参数的分析,有些地方可能比较深入,有些地方可能需要结合本身用这块内存涉及的 JVM 模块去说,会放在另一系列文章详细描述。最后,洗稿抄袭狗不得 house

本篇全篇目录(以及涉及的 JVM 参数):

  1. 从 Native Memory Tracking 说起(全网最硬核 JVM 内存解析 - 1.从 Native Memory Tracking 说起开始)
    1. Native Memory Tracking 的开启
    2. Native Memory Tracking 的使用(涉及 JVM 参数:NativeMemoryTracking
    3. Native Memory Tracking 的 summary 信息每部分含义
    4. Native Memory Tracking 的 summary 信息的持续监控
    5. 为何 Native Memory Tracking 中申请的内存分为 reserved 和 committed
  2. JVM 内存申请与使用流程(全网最硬核 JVM 内存解析 - 2.JVM 内存申请与使用流程开始)
    1. Linux 下内存管理模型简述
    2. JVM commit 的内存与实际占用内存的差异
      1. JVM commit 的内存与实际占用内存的差异
    3. 大页分配 UseLargePages(全网最硬核 JVM 内存解析 - 3.大页分配 UseLargePages开始)
      1. Linux 大页分配方式 - Huge Translation Lookaside Buffer Page (hugetlbfs)
      2. Linux 大页分配方式 - Transparent Huge Pages (THP)
      3. JVM 大页分配相关参数与机制(涉及 JVM 参数:UseLargePages,UseHugeTLBFS,UseSHM,UseTransparentHugePages,LargePageSizeInBytes
  3. Java 堆内存相关设计(全网最硬核 JVM 内存解析 - 4.Java 堆内存大小的确认开始)
    1. 通用初始化与扩展流程
    2. 直接指定三个指标的方式(涉及 JVM 参数:MaxHeapSize,MinHeapSize,InitialHeapSize,Xmx,Xms
    3. 不手动指定三个指标的情况下,这三个指标(MinHeapSize,MaxHeapSize,InitialHeapSize)是如何计算的
    4. 压缩对象指针相关机制(涉及 JVM 参数:UseCompressedOops)(全网最硬核 JVM 内存解析 - 5.压缩对象指针相关机制开始)
      1. 压缩对象指针存在的意义(涉及 JVM 参数:ObjectAlignmentInBytes
      2. 压缩对象指针与压缩类指针的关系演进(涉及 JVM 参数:UseCompressedOops,UseCompressedClassPointers
      3. 压缩对象指针的不同模式与寻址优化机制(涉及 JVM 参数:ObjectAlignmentInBytes,HeapBaseMinAddress
    5. 为何预留第 0 页,压缩对象指针 null 判断擦除的实现(涉及 JVM 参数:HeapBaseMinAddress
    6. 结合压缩对象指针与前面提到的堆内存限制的初始化的关系(涉及 JVM 参数:HeapBaseMinAddress,ObjectAlignmentInBytes,MinHeapSize,MaxHeapSize,InitialHeapSize
    7. 使用 jol jhsdb JVM 日志查看压缩对象指针与 Java 堆验证我们前面的结论
      1. 验证 32-bit 压缩指针模式
      2. 验证 Zero based 压缩指针模式
      3. 验证 Non-zero disjoint 压缩指针模式
      4. 验证 Non-zero based 压缩指针模式
    8. 堆大小的动态伸缩(涉及 JVM 参数:MinHeapFreeRatio,MaxHeapFreeRatio,MinHeapDeltaBytes)(全网最硬核 JVM 内存解析 - 6.其他 Java 堆内存相关的特殊机制开始)
    9. 适用于长期运行并且尽量将所有可用内存被堆使用的 JVM 参数 AggressiveHeap
    10. JVM 参数 AlwaysPreTouch 的作用
    11. JVM 参数 UseContainerSupport - JVM 如何感知到容器内存限制
    12. JVM 参数 SoftMaxHeapSize - 用于平滑迁移更耗内存的 GC 使用
  4. JVM 元空间设计(全网最硬核 JVM 内存解析 - 7.元空间存储的元数据开始)
    1. 什么是元数据,为什么需要元数据
    2. 什么时候用到元空间,元空间保存什么
      1. 什么时候用到元空间,以及释放时机
      2. 元空间保存什么
    3. 元空间的核心概念与设计(全网最硬核 JVM 内存解析 - 8.元空间的核心概念与设计开始)
      1. 元空间的整体配置以及相关参数(涉及 JVM 参数:MetaspaceSize,MaxMetaspaceSize,MinMetaspaceExpansion,MaxMetaspaceExpansion,MaxMetaspaceFreeRatio,MinMetaspaceFreeRatio,UseCompressedClassPointers,CompressedClassSpaceSize,CompressedClassSpaceBaseAddress,MetaspaceReclaimPolicy
      2. 元空间上下文 MetaspaceContext
      3. 虚拟内存空间节点列表 VirtualSpaceList
      4. 虚拟内存空间节点 VirtualSpaceNodeCompressedClassSpaceSize
      5. MetaChunk
        1. ChunkHeaderPool 池化 MetaChunk 对象
        2. ChunkManager 管理空闲的 MetaChunk
      6. 类加载的入口 SystemDictionary 与保留所有 ClassLoaderDataClassLoaderDataGraph
      7. 每个类加载器私有的 ClassLoaderData 以及 ClassLoaderMetaspace
      8. 管理正在使用的 MetaChunkMetaspaceArena
      9. 元空间内存分配流程(全网最硬核 JVM 内存解析 - 9.元空间内存分配流程开始)
        1. 类加载器到 MetaSpaceArena 的流程
        2. MetaChunkArena 普通分配 - 整体流程
        3. MetaChunkArena 普通分配 - FreeBlocks 回收老的 current chunk 与用于后续分配的流程
        4. MetaChunkArena 普通分配 - 尝试从 FreeBlocks 分配
        5. MetaChunkArena 普通分配 - 尝试扩容 current chunk
        6. MetaChunkArena 普通分配 - 从 ChunkManager 分配新的 MetaChunk
        7. MetaChunkArena 普通分配 - 从 ChunkManager 分配新的 MetaChunk - 从 VirtualSpaceList 申请新的 RootMetaChunk
        8. MetaChunkArena 普通分配 - 从 ChunkManager 分配新的 MetaChunk - 将 RootMetaChunk 切割成为需要的 MetaChunk
        9. MetaChunk 回收 - 不同情况下, MetaChunk 如何放入 FreeChunkListVector
      10. ClassLoaderData 回收
    4. 元空间分配与回收流程举例(全网最硬核 JVM 内存解析 - 10.元空间分配与回收流程举例开始)
      1. 首先类加载器 1 需要分配 1023 字节大小的内存,属于类空间
      2. 然后类加载器 1 还需要分配 1023 字节大小的内存,属于类空间
      3. 然后类加载器 1 需要分配 264 KB 大小的内存,属于类空间
      4. 然后类加载器 1 需要分配 2 MB 大小的内存,属于类空间
      5. 然后类加载器 1 需要分配 128KB 大小的内存,属于类空间
      6. 新来一个类加载器 2,需要分配 1023 Bytes 大小的内存,属于类空间
      7. 然后类加载器 1 被 GC 回收掉
      8. 然后类加载器 2 需要分配 1 MB 大小的内存,属于类空间
    5. 元空间大小限制与动态伸缩(全网最硬核 JVM 内存解析 - 11.元空间分配与回收流程举例开始)
      1. CommitLimiter 的限制元空间可以 commit 的内存大小以及限制元空间占用达到多少就开始尝试 GC
      2. 每次 GC 之后,也会尝试重新计算 _capacity_until_GC
    6. jcmd VM.metaspace 元空间说明、元空间相关 JVM 日志以及元空间 JFR 事件详解(全网最硬核 JVM 内存解析 - 12.元空间各种监控手段开始)
      1. jcmd <pid> VM.metaspace 元空间说明
      2. 元空间相关 JVM 日志
      3. 元空间 JFR 事件详解
        1. jdk.MetaspaceSummary 元空间定时统计事件
        2. jdk.MetaspaceAllocationFailure 元空间分配失败事件
        3. jdk.MetaspaceOOM 元空间 OOM 事件
        4. jdk.MetaspaceGCThreshold 元空间 GC 阈值变化事件
        5. jdk.MetaspaceChunkFreeListSummary 元空间 Chunk FreeList 统计事件
  5. JVM 线程内存设计(重点研究 Java 线程)(全网最硬核 JVM 内存解析 - 13.JVM 线程内存设计开始)
    1. JVM 中有哪几种线程,对应线程栈相关的参数是什么(涉及 JVM 参数:ThreadStackSize,VMThreadStackSize,CompilerThreadStackSize,StackYellowPages,StackRedPages,StackShadowPages,StackReservedPages,RestrictReservedStack
    2. Java 线程栈内存的结构
    3. Java 线程如何抛出的 StackOverflowError
      1. 解释执行与编译执行时候的判断(x86为例)
      2. 一个 Java 线程 Xss 最小能指定多大

3. Java 堆内存相关设计

3.1. 通用初始化与扩展流程

目前最新的 JVM,主要根据三个指标初始化堆以及扩展或缩小堆:

  • 最大堆大小
  • 最小堆大小
  • 初始堆大小

不同的 GC 情况下,初始化以及扩展的流程可能在某些细节不太一样,但是,大体的思路都是:

  1. 初始化阶段,reserve 最大堆大小,并且 commit 初始堆大小
  2. 在某些 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

代码语言:javascript复制
#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

代码语言:javascript复制
//如果设置了 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: %sn", 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: %sn", 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 启动参数,同一个参数可以多次出现,但是只有最后一个会生效,例如:

代码语言:javascript复制
java -XX:MaxHeapSize=8G -XX:MaxHeapSize=4G -XX:MaxHeapSize=8M -version

这个命令启动的 JVM MaxHeapSize 为 8MB。由于前面提到 Xmx 与 MaxHeapSize 是等价的,所以这么写也是可以的(虽然最后 MaxHeapSize 还是 8MB):

代码语言:javascript复制
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 或者 XmxMaxHeapSize 也有自己预设的一个参考值。源码中这个预设参考值为 125MB 左右(96M*13/10)。但是一般最后不会以这个参考值为准,JVM 初始化的时候会有很复杂的计算计算出合适的值。比如你可以在你的电脑上执行下下面的命令,可以看到类似下面的输出:

代码语言:javascript复制
>  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,默认是 2
  • MaxRAMFraction: 已过期,如果配置了会转化为 MaxRAMPercentage 换算关系是:MaxRAMPercentage = 100.0 / MaxRAMFraction,默认是 4

对应的源码是:https://github.com/openjdk/jdk/blob/jdk-21 3/src/hotspot/share/gc/shared/gc_globals.hpp

代码语言:javascript复制
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

代码语言:javascript复制
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)                                               

0 人点赞