2024-01-25  阅读(38)
原文作者:hashcon 原文地址: https://zhanghaoxin.blog.csdn.net/article/details/130377687

本篇是关于 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

    #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

3.3. 不手动指定三个指标的情况下,这三个指标(MinHeapSize,MaxHeapSize,InitialHeapSize)是如何计算的

上一章节我们提到我们可以手动指定这三个参数,如果不指定呢?JVM 会怎么计算这三个指标的大小?首先,当然,JVM 会读取 JVM 可用内存 :首先 JVM 需要知道自己可用多少内存,我们称为 可用内存 。由此引入第一个 JVM 参数,MaxRAM,这个参数是用来明确指定 JVM 进程可用内存大小的,如果没有指定,JVM 会自己读取系统可用内存。这个可用内存用来指导 JVM 限制最大堆内存。后面我们会看到很多 JVM 参数与这个可用内存相关。

前面我们还提到了,就算没有指定 MaxHeapSize 或者 XmxMaxHeapSize 也有自己预设的一个参考值。源码中这个 预设参考值 为 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 的计算流程:

202401252015293801.png

流程中涉及了以下几个参数,还有一些已经过期的参数,会被转换成未过期的参数:

  • 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

    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,也会经过下面的计算过程计算出来:

202401252015297372.png

流程中涉及了以下几个参数,还有一些已经过期的参数,会被转换成未过期的参数:

  • 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)                                               \

微信搜索“干货满满张哈希”关注公众号,加作者微信,每日一刷,轻松提升技术,斩获各种offer
我会经常发一些很好的各种框架的官方社区的新闻视频资料并加上个人翻译字幕到如下地址(也包括上面的公众号),欢迎关注:

  • 知乎:https://www.zhihu.com/people/zhxhash
  • B 站:https://space.bilibili.com/31359187

Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文