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

本篇是关于 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 最小能指定多大

4. JVM 元空间设计

4.1. 什么是元数据,为什么需要元数据

JVM 在执行 Java 应用程序时,将加载的 Java 类的许多细节记录在内存中,这些信息称为类 元数据Class MetaData )。这些元数据对于 Java 的很多灵活的语言以及虚拟机特性都是很重要的,比如动态类加载、JIT 实时编译、反射以及动态代理等等。不同的 JVM 加载类保存的内存信息是不一样的,它们通常 在更低的内存占用更快的执行速度之间进行权衡 (类似于空间还是时间的权衡)。对于 OpenJDK Hotspot 使用的则是相对丰富的元数据模型来获得尽可能快的性能(时间优先,不影响速度的情况下尽量优化空间占用)。相比于 C,C++,Go 这些离线编译为可执行二进制文件的程序相比,像 JVM 这样的托管运行时动态解释执行或者编译执行的, 则需要保留更多关于正在执行的代码的运行时信息 。原因如下:

  1. 依赖类库并不是一个确定的有限集 :Java 可以动态加载类,并且还有 ASM 以及 Javassist 这些工具在运行时动态定义类并加载,还有 JVMTI agent 这样的机制来动态修改类。所以,JVM 通过类元数据保存: 运行时中存在哪些类,它们包含哪些方法和字段,并能够在链接加载期间动态地解析从一个类到另一个类的引用 。类的链接也需要考虑类的 可见性和可访问性 。类元数据 与类加载器相关联 ,同时类元数据也包括 类权限和包路径以及模块信息 (Java 9之后引入的模块化),以确定可访问性
  2. JVM 解释执行或者通过 JIT 实时编译执行 Java 代码的时候需要基于类元数据的很多信息才能执行 :需要知道例如类与类之间的关系,类属性以及字段还有方法结构等等等等。例如在做强制转换的时候,需要检查类型的父子类关系确定是否可以强制转换等等。
  3. JVM 需要一些统计数据决定哪些代码解释执行那些代码是热点代码需要 JIT 即时编译执行
  4. Java 有反射 API 供用户使用 ,这就需要运行时知道所有类的各种信息。洗稿也是一种侵权行为

4.2. 什么时候用到元空间,元空间保存什么

4.2.1. 什么时候用到元空间,以及释放时机

只要发生类加载,就会用到元空间 。例如我们创建一个类对象时:这个类首先会被类加载器加载,在发生类加载的时候,对应类的元数据被存入元空间。元数据分为 两部分存入元空间,一部分存入了元空间的类空间另一部分存入了元空间的非类空间 。堆中新建的对象的对象头中的 Klass 指针部分,指向元空间中 Klass,同时,Klass 中各种字段都是指针,实际对象的地址,可能在非类空间,例如实现方法多态以及 virtual call 的 vtable 与 itable 保存着方法代码地址的引用指针。非类空间中存储着比较大的元数据,例如常量池,字节码,JIT 编译后的代码等等。由于编译后的代码可能非常大,以及 JVM 对于多语言支持的扩展可能动态加载很多类,所以 将 MetaSpace 的类空间与非类空间区分开 。如图所示:

202401252015429311.png

JVM 启动参数 -XX:CompressedClassSpaceSize 指定的是压缩类空间大小,默认是 1G。-XX:MaxMetaspaceSize控制的是 MetaSpace 的总大小。这两个参数,以及 MetaSpace 更多参数,我们会在后面的章节详细解释。

当类加载器加载的 所有类都没有任何实例并且没有任何指向 这些类对象(java.lang.Class)的引用,也 没有指向这个类加载器 的引用的时候,如果发生了 GC,这个类加载器使用的元空间就会被释放。但是这个释放并 不一定是释放回操作系统,而是被标记为可以被其他类加载器使用 了。

4.2.2. 元空间保存什么

元空间保存的 数据,目前分为两大类

  • Java 类数据 :即加载的 Java 类对应 JVM 中的 Klass 对象(Klass 是 JVM 源码中的一个 c++ 类,你可以理解为类在 JVM 中的内存形式),但是这个 Klass 对象中存储的很多数据都是指针,具体的数据存储属于 非 Java 类数据一般非 Java 类数据远比 Java 类数据占用空间大
  • 非 Java 类数据 :即被 Klass 对象引用的一些数据,例如:类中的各种方法,注解,执行采集与统计信息等等。不要偷取他人的劳动成果,也不要浪费自己的时间和精力,让我们一起做一个有良知的写作者。

如果是 64 位的 JVM 虚拟机(从 Java 9+ 开始只有 64 位的虚拟机了)并且开启了 压缩类指针(-XX:+UseCompressedClassPointers,默认是开启的),那么元空间会被划分成两部分

  • 类元空间 :存储上面说的 Java 类数据 的空间
  • 数据元空间 :存储上面说的 非 Java 类数据 的空间

基于是否开启了压缩类指针分为这两部分的原因 是,(剽窃抄袭侵权
)在对象头需要保留指向 Klass 的指针,如果我们能尽量压缩这个指针的大小,那么每个对象的大小也能得到压缩,这将节省很多堆空间。在 64 位虚拟机上面,指针默认都是 64 位大小的,开启压缩类指针(-XX:+UseCompressedClassPointers,默认是开启的)之后,类指针变为 32 位大小,最多能指向 2^32 也就是 4G 的空间,如果我们能保持 Klass 所处的空间占用不超过这个限制的话,就能使用压缩类指针了。所以我们把 Klass 单独提取到一个单独的区域进行分配。Klass 占用的空间并不会太大,虽然对于 Java 中的每一个类都会有一个 Klass,但是占用空间的方法内容以及动态编译信息等等,具体数据都在 数据元空间 中存储,Klass 中大部分都是指针。基本上很少会遇到 32 位指针不够用的情况。

注意,老版本中, UseCompressedClassPointers 取决于 UseCompressedOops,即压缩对象指针如果没开启,那么压缩类指针也无法开启。但是 从 Java 15 Build 23 开始, UseCompressedClassPointers 已经不再依赖 UseCompressedOops ,两者在大部分情况下已经独立开来。除非在 x86 的 CPU 上面启用 JVM Compiler Interface(例如使用 GraalVM)。参考 JDK ISSUE:https://bugs.openjdk.java.net/browse/JDK-8241825 - Make compressed oops and compressed class pointers independent (x86_64, PPC, S390) 以及源码:

在元空间分配的对象,都是调用 Metaspace::allocate 从元空间分配空间。调用这个方法的是 MetaspaceObj 的构造函数,对应源码:https://github.com/openjdk/jdk/blob/jdk-21+3/src/hotspot/share/memory/allocation.cpp

    void* MetaspaceObj::operator new(size_t size, ClassLoaderData* loader_data,
                                     size_t word_size,
                                     MetaspaceObj::Type type, TRAPS) throw() {
      // Klass has its own operator new
      return Metaspace::allocate(loader_data, word_size, type, THREAD);
    }//你以为我想这样么?主要是抄袭狗太多
    
    void* MetaspaceObj::operator new(size_t size, ClassLoaderData* loader_data,
                                     size_t word_size,
                                     MetaspaceObj::Type type) throw() {
      assert(!Thread::current()->is_Java_thread(), "only allowed by non-Java thread");
      return Metaspace::allocate(loader_data, word_size, type);
    }

MetaspaceObj 的 Operator new 方法定义了从 MetaSpace 上分配内存,即所有 MetaspaceObj 的子类,只要没有明确覆盖从其他地方分配,就会从 MetaSpace 分配内存。MetaspaceObj 的子类包括:

位于类元空间的

  • Klass:其实就是 Java 类的实例(每个 Java 的 class 有一个对应的对象实例,用来反射访问,这个就是那个对象实例),即 Java 对象头的类型指针指向的实例:

位于数据元空间的

  • Symbol:符号常量,即类中所有的符号字符串,例如类名称,方法名称,方法定义等等。
  • ConstantPool:运行时常量池,数据来自于类文件中的常量池。
  • ConstanPoolCache:运行时常量池缓存,用于加速常量池访问
  • ConstMethod:类文件中的方法解析后,静态信息放入 ConstMethod,这部分信息可以理解为是不变的,例如字节码,行号,方法异常表,本地变量表,参数表等等。
  • MethodCounters:方法的计数器相关数据。
  • MethodData:方法数据采集,动态编译相关数据。例如某个方法需要采集一些指标,决定是否采用 C1 C2 动态编译优化性能。
  • Method:Java 方法,包含以上 ConstMethodMethodCountersMethodData 的指针以及一些额外数据。
  • RecordComponent:对应 Java 14 新特性 Record,即从 Record 中解析出的关键信息。

以上这类型,我们在下一个系列 全网最硬核 JVM 元空间解析中再详细说明

微信搜索“干货满满张哈希”关注公众号,加作者微信,每日一刷,轻松提升技术,斩获各种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] ,回复【面试题】 即可免费领取。

阅读全文