全网最硬核 JVM 内存解析 - 1.从 Native Memory Tracking 说起
个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判。如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 issue,谢谢支持~
另外,本文为了避免抄袭,会在不影响阅读的情况下,在文章的随机位置放入对于抄袭和洗稿的人的“亲切”的问候。如果是正常读者看到,笔者在这里说声对不起,。如果被抄袭狗或者洗稿狗看到了,希望你能够好好反思,不要再抄袭了,谢谢。
今天又是干货满满的一天,这是全网最硬核 JVM 解析系列第四篇,往期精彩:
本篇是关于 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 参数:
1. 从 Native Memory Tracking 说起
JVM 内存究竟包括哪些,可能网上众说纷纭。我们这里由官方提供的一个查看 JVM 内存占用的工具引入,即 Native Memory Tracking。不过要注意的一点是,这个只能监控 JVM 原生申请的内存大小,如果是通过 JDK 封装的系统 API 申请的内存,是统计不到的,例如 Java JDK 中的 DirectBuffer 以及 MappedByteBuffer 这两个(当然,对于这两个,我们后面也有其他的办法去看到当前使用的大小。当然xigao dog 啥都不会)。以及如果你自己封装 JNI 调用系统调用去申请内存,都是 Native Memory Tracking 无法涵盖的。这点要注意。
1.1. Native Memory Tracking 的开启
Native Memory Tracking 主要是用来通过在 JVM 向系统申请内存的时候进行埋点实现的。注意,这个埋点,并不是完全没有消耗的,我们后面会看到。由于需要埋点,并且 JVM 中申请内存的地方很多,这个埋点是有不小消耗的,这个 Native Memory Tracking 默认是不开启的,并且无法动态开启(因为这是埋点采集统计的,如果可以动态开启那么没开启的时候的内存分配没有记录无法知晓,所以无法动态开启),目前只能通过在启动 JVM 的时候通过启动参数开启。即通过 -XX:NativeMemoryTracking
开启:
-XX:NativeMemoryTracking=off
:这是默认值,即关闭 Native Memory Tracking-XX:NativeMemoryTracking=summary
: 开启 Native Memory Tracking,但是仅仅按照各个 JVM 子系统去统计内存占用情况-XX:NativeMemoryTracking=detail
:开启 Native Memory Tracking,从每次 JVM 中申请内存的不同调用路径的维度去统计内存占用情况。注意,开启 detail 比开启 summary 的消耗要大不少,因为 detail 每次都要解析 CallSite 分辨调用位置。我们一般用不到这么详细的内容,除非是 JVM 开发。只有洗稿狗才会开启这个配置导致线上崩溃而自己又很懵。
开启之后,我们可以通过 jcmd 命令去查看 Native Memory Tracking 的信息,即jcmd <pid> VM.native_memory
:
jcmd <pid> VM.native_memory
或者jcmd <pid> VM.native_memory summary
:两者是等价的,即查看 Native Memory Tracking 的 summary 信息。默认单位是 KB,可以指定单位为其他,例如jcmd <pid> VM.native_memory summary scale=MB
jcmd <pid> VM.native_memory detail
:查看 Native Memory Tracking 的 detail 信息,包括 summary 信息,以及按照虚拟内存映射分组的内存使用信息,还有按照不同 CallSite 调用分组的内存使用情况。默认单位是 KB,可以指定单位为其他,例如jcmd <pid> VM.native_memory detail scale=MB
1.2. Native Memory Tracking 的使用
对于我们这些 Java 开发以及 JVM 使用者而言(对于抄袭狗是没有好果汁吃的),我们只关心并且查看 Native Memory Tracking 的 summary 信息即可,detail 信息一般是供 JVM 开发人员使用的,我们不用太关心,我们后面的分析也只会涉及 Native Memory Tracking 的 summary 部分。
一般地,只有遇到问题的时候,我们才会考虑开启 Native Memory Tracking,并且在定位出问题后,我们想把它关闭,可以通过 jcmd <pid> VM.native_memory shutdown
进行关闭并清理掉之前 Native Memory tracking 使用的埋点以及占用的内存。如前面所述,我们无法动态开启 Native Memory tracking,所以只要动态关闭了,这个进程就无法再开启了。
jcmd 本身提供了简单的对比功能,例如:
- 使用
jcmd <pid> VM.native_memory baseline
记录当前内存占用信息 - 之后过一段时间
jcmd <pid> VM.native_memory summary.diff
会输出当前 Native Memory Tracking 的 summary 信息,如果与第一步 baseline 的有差异,会在对应位将差异输出
但是这个工具本身比较粗糙,我们有时候并不知道何时调用 jcmd <pid> VM.native_memory summary.diff
合适,因为我们不确定什么时候会有我们想看到的内存使用过大的问题。所以我们一般做成一种持续监控的方式
1.3. Native Memory Tracking 的 summary 信息每部分含义
以下是一个 Native Memory Tracking 的示例输出:
Total: reserved=10575644KB, committed=443024KB
- Java Heap (reserved=8323072KB, committed=192512KB)
(mmap: reserved=8323072KB, committed=192512KB)
- Class (reserved=1050202KB, committed=10522KB)
(classes 15409)
( instance classes 14405, array classes 1004)
(malloc=1626KB 33495)
(mmap: reserved=1048576KB, committed=8896KB)
( Metadata: )
( reserved=57344KB, committed=57216KB)
( used=56968KB)
( waste=248KB =0.43%)
( Class space:)
( reserved=1048576KB, committed=8896KB)
( used=8651KB)
( waste=245KB =2.75%)
- Thread (reserved=669351KB, committed=41775KB)
(thread 653)
(stack: reserved=667648KB, committed=40072KB)
(malloc=939KB 3932)
(arena=764KB 1304)
- Code (reserved=50742KB, committed=17786KB)
(malloc=1206KB 9495)
(mmap: reserved=49536KB, committed=16580KB)
- GC (reserved=370980KB, committed=69260KB)
(malloc=28516KB 8340)
(mmap: reserved=342464KB, committed=40744KB)
- Compiler (reserved=159KB, committed=159KB)
(malloc=29KB 813)
(arena=131KB 3)
- Internal (reserved=1373KB, committed=1373KB)
(malloc=1309KB 6135)
(mmap: reserved=64KB, committed=64KB)
- Other (reserved=12348KB, committed=12348KB)
(malloc=12348KB 14)
- Symbol (reserved=18629KB, committed=18629KB)
(malloc=16479KB 445877)
(arena=2150KB 1)
- Native Memory Tracking (reserved=8426KB, committed=8426KB)
(malloc=325KB 4777)
(tracking overhead=8102KB)
- Shared class space (reserved=12032KB, committed=12032KB)
(mmap: reserved=12032KB, committed=12032KB)
- Arena Chunk (reserved=187KB, committed=187KB)
(malloc=187KB)
- Tracing (reserved=32KB, committed=32KB)
(arena=32KB 1)
- Logging (reserved=5KB, committed=5KB)
(malloc=5KB 216)
- Arguments (reserved=31KB, committed=31KB)
(malloc=31KB 90)
- Module (reserved=403KB, committed=403KB)
(malloc=403KB 2919)
- Safepoint (reserved=8KB, committed=8KB)
(mmap: reserved=8KB, committed=8KB)
- Synchronization (reserved=56KB, committed=56KB)
(malloc=56KB 789)
- Serviceability (reserved=1KB, committed=1KB)
(malloc=1KB 18)
- Metaspace (reserved=57606KB, committed=57478KB)
(malloc=262KB 180)
(mmap: reserved=57344KB, committed=57216KB)
- String Deduplication (reserved=1KB, committed=1KB)
(malloc=1KB 8)
我们接下来将上面的信息按不同子系统分别简单分析下其含义:
1.Java堆内存,所有 Java 对象分配占用内存的来源,由 JVM GC 管理回收,这是我们在第三章会重点分析的:
//堆内存占用,reserve 了 8323072KB,当前 commit 了 192512KB 用于实际使用
Java Heap (reserved=8323072KB, committed=192512KB)
//堆内存都是通过 mmap 系统调用方式分配的
(mmap: reserved=8323072KB, committed=192512KB)
//chao xi 可耻
2.元空间,JVM 将类文件加载到内存中用于后续使用占用的空间,注意是 JVM C++ 层面的内存占用,主要包括类文件中在 JVM 解析为 C++ 的 Klass 类以及相关元素。对应的 Java 反射类 Class 还是在堆内存空间中:
//Class 是类元空间总占用,reserve 了 1050202KB,当前 commit 了 10522KB 用于实际使用
//总共 reserved 1050202KB = mmap reserved 1048576KB + malloc 1626KB
//总共 committed 10522KB = mmap committed 8896KB + malloc 1626KB
Class (reserved=1050202KB, committed=10522KB)
(classes 15409) //一共加载了 15409 个类
( instance classes 14405, array classes 1004) //其中 14405 个实体类,1004 个数组类
(malloc=1626KB 33495) //通过 malloc 系统调用方式一共分配了 1626KB,一共调用了 33495 次 malloc
(mmap: reserved=1048576KB, committed=8896KB) //通过 mmap 系统调用方式 reserve 了 1048576KB,当前 commit 了 8896KB 用于实际使用
( Metadata: )//注意,MetaData 这块不属于类元空间,属于数据元空间,后面第四章会详细分析
( reserved=57344KB, committed=57216KB) //数据元空间当前 reserve 了 57344KB,commit 了 57216KB 用于实际使用
( used=56968KB) //但是实际从 MetaChunk 的角度去看使用,只有 56968KB 用于实际数据的分配,有 248KB 的浪费
( waste=248KB =0.43%)
( Class space:)
( reserved=1048576KB, committed=8896KB) //类元空间当前 reserve 了 1048576KB,commit 了 8896KB 用于实际使用
( used=8651KB) //但是实际从 MetaChunk 的角度去看使用,只有 8651KB 用于实际数据的分配,有 245KB 的浪费
( waste=245KB =2.75%)
洗稿去shi
Shared class space (reserved=12032KB, committed=12032KB) //共享类空间,当前 reserve 了 12032KB,commit 了 12032KB 用于实际使用,这块其实属于上面 Class 的一部分
(mmap: reserved=12032KB, committed=12032KB)
Module (reserved=403KB, committed=403KB) //加载并记录模块占用空间,当前 reserve 了 403KB,commit 了 403KB 用于实际使用
(malloc=403KB 2919)
Metaspace (reserved=57606KB, committed=57478KB) //等价于上面 Class 中的 MetaChunk(除了 malloc 的部分),当前 reserve 了 57606KB,commit 了 57478KB 用于实际使用
(malloc=262KB 180)
(mmap: reserved=57344KB, committed=57216KB)
3.C++ 字符串即符号(Symbol)占用空间,前面加载类的时候,其实里面有很多字符串信息(注意不是 Java 字符串,是 JVM 层面 C++ 字符串),不同类的字符串信息可能会重复(维护原创打死潮汐犬)。所以统一放入符号表(Symbol table)复用。元空间中保存的是针对符号表中符号的引用。这不是本期内容的重点,我们不会详细分析
Symbol (reserved=18629KB, committed=18629KB)
(malloc=16479KB 445877) //通过 malloc 系统调用方式一共分配了 16479KB,一共调用了 445877 次 malloc
(arena=2150KB 1) //通过 arena 系统调用方式一共分配了 2150KB,一共调用了 1 次 arena
4.线程占用内存,主要是每个线程的线程栈,我们也只会主要分析线程栈占用空间(在第五章),其他的管理线程占用的空间很小,可以忽略不计。
//总共 reserve 了 669351KB,commit 了 41775KB
Thread (reserved=669351KB, committed=41775KB)
(thread 653)//当前线程数量是 653
(stack: reserved=667648KB, committed=40072KB) //线程栈占用的空间:我们没有指定 Xss,默认是 1MB,所以 reserved 是 653 * 1024 = 667648KB,当前 commit 了 40072KB 用于实际使用
(malloc=939KB 3932) //通过 malloc 系统调用方式一共分配了 939KB,一共调用了 3932 次 malloc
(arena=764KB 1304) //通过 JVM 内部 Arena 分配的内存,一共分配了 764KB,一共调用了 1304 次 Arena 分配
5.JIT编译器本身占用的空间以及JIT编译器编译后的代码占用空间,这也不是本期内容的重点,我们不会详细分析
Code (reserved=50742KB, committed=17786KB)
(malloc=1206KB 9495)
(mmap: reserved=49536KB, committed=16580KB)
//chao xi 直接去火葬场炒
Compiler (reserved=159KB, committed=159KB)
(malloc=29KB 813)
(arena=131KB 3)
6.Arena 数据结构占用空间,我们看到 Native Memory Tracking 中有很多通过 arena 分配的内存,这个就是管理 Arena 数据结构占用空间。这不是本期内容的重点,我们不会详细分析
Arena Chunk (reserved=187KB, committed=187KB)
(malloc=187KB)
7.JVM Tracing 占用内存,包括 JVM perf 以及 JFR 占用的空间。其中 JFR 占用的空间可能会比较大,我在我的另一个关于 JFR 的系列里面分析过 JVM 内存中占用的空间。这不是本期内容的重点,我们不会详细分析
Tracing (reserved=32KB, committed=32KB)
(arena=32KB 1)
8.写 JVM 日志占用的内存(-Xlog
参数指定的日志输出,并且 Java 17 之后引入了异步 JVM 日志-Xlog:async
,异步日志所需的 buffer 也在这里),这不是本期内容的重点,我们不会详细分析
Logging (reserved=5KB, committed=5KB)
(malloc=5KB 216)
9.JVM 参数占用内存,我们需要保存并处理当前的 JVM 参数以及用户启动 JVM 的是传入的各种参数(有时候称为 flag)。这不是本期内容的重点,我们不会详细分析
Arguments (reserved=31KB, committed=31KB)
(malloc=31KB 90)
10.JVM 安全点占用内存,是固定的两页内存(我这里是一页是 4KB,后面第二章会分析这个页大小与操作系统相关),用于 JVM 安全点的实现,不会随着 JVM 运行时的内存占用而变化。JVM 安全点请期待本系列文章的下一系列:全网最硬核的 JVM 安全点与线程握手机制解析。这不是本期内容的重点,我们不会详细分析
Safepoint (reserved=8KB, committed=8KB)
(mmap: reserved=8KB, committed=8KB)
11.Java 同步机制(例如 synchronized
,还有 AQS 的基础 LockSupport
)底层依赖的 C++ 的数据结构,系统内部的 mutex 等占用的内存。这不是本期内容的重点,我们不会详细分析
Synchronization (reserved=56KB, committed=56KB)
(malloc=56KB 789)
12.JVM TI 相关内存,JVMTI 是 Java 虚拟机工具接口(Java Virtual Machine Tool Interface)的缩写。它是 Java 虚拟机(JVM)的一部分,提供了一组 API,使开发人员可以开发自己的 Java 工具和代理程序,以监视、分析和调试 Java 应用程序。JVMTI API 是一组 C/C++ 函数,可以通过 JVM TI Agent Library 和 JVM 进行交互。开发人员可以使用 JVMTI API 开发自己的 JVM 代理程序或工具,以监视和操作 Java 应用程序。例如,可以使用 JVMTI API 开发性能分析工具、代码覆盖率工具、内存泄漏检测工具等等。这里的内存就是调用了 JVMTI API 之后 JVM 为了生成数据占用的内存。这不是本期内容的重点,我们不会详细分析
Serviceability (reserved=1KB, committed=1KB)
(malloc=1KB 18)
13.Java 字符串去重占用内存:Java 字符串去重机制可以减少应用程序中字符串对象的内存占用。 在 Java 应用程序中,字符串常量是不可变的,并且通常被使用多次。这意味着在应用程序中可能存在大量相同的字符串对象,这些对象占用了大量的内存。Java 字符串去重机制通过在堆中共享相同的字符串对象来解决这个问题。当一个字符串对象被创建时,JVM 会检查堆中是否已经存在相同的字符串对象。如果存在,那么新的字符串对象将被舍弃,而引用被返回给现有的对象。这样就可以减少应用程序中字符串对象的数量,从而减少内存占用。 但是这个机制一直在某些 GC 下表现不佳,尤其是 G1GC 以及 ZGC 中,所以默认是关闭的,可以通过 -XX:+UseStringDeduplication
来启用。这不是本期内容的重点,我们不会详细分析。
String Deduplication (reserved=1KB, committed=1KB)
(malloc=1KB 8)
14.JVM GC需要的数据结构与记录信息占用的空间,这块内存可能会比较大,尤其是对于那种专注于低延迟的 GC,例如 ZGC。其实 ZGC 是一种以空间换时间的思路,提高 CPU 消耗与内存占用,但是消灭全局暂停。之后的 ZGC 优化方向就是尽量降低 CPU 消耗与内存占用,相当于提高了性价比。这不是本期内容的重点,我们不会详细分析。
GC (reserved=370980KB, committed=69260KB)
(malloc=28516KB 8340)
(mmap: reserved=342464KB, committed=40744KB)
15.JVM内部(不属于其他类的占用就会归到这一类)与其他占用(不是 JVM 本身而是操作系统的某些系统调用导致额外占的空间),不会很大
Internal (reserved=1373KB, committed=1373KB)
(malloc=1309KB 6135)
(mmap: reserved=64KB, committed=64KB)
Other (reserved=12348KB, committed=12348KB)
(malloc=12348KB 14)
16.开启 Native Memory Tracking 本身消耗的内存,这个就不用多说了吧
Native Memory Tracking (reserved=8426KB, committed=8426KB)
(malloc=325KB 4777)
(tracking overhead=8102KB)
1.4. Native Memory Tracking 的 summary 信息的持续监控
现在 JVM 一般大部分部署在 k8s 这种云容器编排的环境中,每个 JVM 进程内存是受限的。如果超过限制,那么会触发 OOMKiller 将这个 JVM 进程杀掉。我们一般都是由于自己的 JVM 进程被 OOMKiller 杀掉,才会考虑打开 NativeMemoryTracking 去看看哪块内存占用比较多以及如何调整的。
OOMKiller 是积分制,并不是你的 JVM 进程一超过限制就立刻会被杀掉,而是超过的话会累积分,累积到一定程度,就可能会被 OOMKiller 杀掉。所以,我们可以通过定时输出 Native Memory Tracking 的 summary 信息,从而抓到超过内存限制的点进行分析。
但是,我们不能仅通过 Native Memory Tracking 的数据就判断 JVM 占用的内存,因为在后面的 JVM 内存申请与使用流程的分析我们会看到,JVM 通过 mmap 分配的大量内存都是先 reserve 再 commit 之后实际往里面写入数据的时候,才会真正分配物理内存。同时,JVM 还会动态释放一些内存,这些内存可能不会立刻被操作系统回收。Native Memory Tracking 是 JVM 认为自己向操作系统申请的内存,与实际操作系统分配的内存是有所差距的,所以我们不能只查看 Native Memory Tracking 去判断,我们还需要查看能体现真正内存占用指标。这里可以查看 linux 进程监控文件 smaps_rollup
看出具体的内存占用,例如 (一般不看 Rss,因为如果涉及多个虚拟地址映射同一个物理地址的话会有不准确,所以主要关注 Pss 即可,但是 Pss 更新不是实时的,但也差不多,这就可以理解为进程占用的实际物理内存):
> cat /proc/23/smaps_rollup
689000000-fffff53a9000 ---p 00000000 00:00 0 [rollup]
Rss: 5870852 kB
Pss: 5849120 kB
Pss_Anon: 5842756 kB
Pss_File: 6364 kB
Pss_Shmem: 0 kB
Shared_Clean: 27556 kB
Shared_Dirty: 0 kB
Private_Clean: 524 kB
Private_Dirty: 5842772 kB
Referenced: 5870148 kB
Anonymous: 5842756 kB
LazyFree: 0 kB
AnonHugePages: 0 kB
ShmemPmdMapped: 0 kB
FilePmdMapped: 0 kB
Shared_Hugetlb: 0 kB
Private_Hugetlb: 0 kB
Swap: 0 kB
SwapPss: 0 kB
Locked: 0 kB
笔者通过在每个 Spring Cloud 微服务进程加入下面的代码,来实现定时的进程内存监控,主要通过 smaps_rollup 查看实际的物理内存占用找到内存超限的时间点,Native Memory Tracking 查看 JVM 每块内存占用的多少,用于指导优化参数。
import lombok.extern.log4j.Log4j2;
import org.apache.commons.io.FileUtils;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import static org.springframework.cloud.bootstrap.BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME;
@Log4j2
public class MonitorMemoryRSS implements ApplicationListener<ApplicationReadyEvent> {
private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false);
private static final ScheduledThreadPoolExecutor sc = new ScheduledThreadPoolExecutor(1);
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
if (isBootstrapContext(event)) {
return;
}
synchronized (INITIALIZED) {
if (INITIALIZED.get()) {
return;
}
sc.scheduleAtFixedRate(() -> {
long pid = ProcessHandle.current().pid();
try {
//读取 smaps_rollup
List<String> strings = FileUtils.readLines(new File("/proc/" + pid + "/smaps_rollup"));
log.info("MonitorMemoryRSS, smaps_rollup: {}", strings.stream().collect(Collectors.joining("\n")));
//读取 Native Memory Tracking 信息
Process process = Runtime.getRuntime().exec(new String[]{"jcmd", pid + "", "VM.native_memory"});
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
log.info("MonitorMemoryRSS, native_memory: {}", reader.lines().collect(Collectors.joining("\n")));
}
} catch (IOException e) {
}
}, 0, 30, TimeUnit.SECONDS);
INITIALIZED.set(true);
}
}
static boolean isBootstrapContext(ApplicationReadyEvent applicationEvent) {
return applicationEvent.getApplicationContext().getEnvironment().getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME);
}
}
同时,笔者还将这些输出抽象为 JFR 事件,效果是:
1.5. 为何 Native Memory Tracking 中申请的内存分为 reserved 和 committed
这个会在第二章详细分析