JVM常用概念之本地内存跟踪

ops/2025/3/12 13:59:00/

问题

Java应用启动或者运行过程中报“内存不足!”,我们该怎么办?

基础知识

对于一个在本地机器运行的JVM应用而言,需要足够的内存来存储机器代码、堆元数据、类元数据、内存分析等数据结构,来保证JVM应用的成功启动以及未来平稳的运行,然而JVM在运行期间会面临各种不同情况下的动态处理,比如动态加载、热编译等会产生大量的类,从而在运行时会产生足够的生成代码,这种情况是JVM默认该应用程序会长期运行的,而对于只是短期运行的JVM应用程序而言是不需要这样处理的。

OpenJDK8以及之后的版本提供了一个叫做本地内存跟踪NMT)的工具,该工具可以知道JVM内部的内存分配,对分析JVM内存相关问题是否有帮助。

我们可以使用 -XX:NativeMemoryTracking=summary 启用 NMT。您可以让 jcmd 转储当前 NMT 数据,或者可以使用-XX:+PrintNMTStatistics在 JVM 终止时请求数据转储。输入 -XX:NativeMemoryTracking=detail 将获取 mmaps 的内存映射和 mallocs 的调用堆栈。

实验

测试用例源码

java">public class Hello {public static void main(String... args) {System.out.println("Hello");}
}

执行结果

JVM参数

-Xmx16m -Xms16m

NMT_27">NMT结果

Native Memory Tracking:Total: reserved=1373921KB, committed=74953KB
-                 Java Heap (reserved=16384KB, committed=16384KB)(mmap: reserved=16384KB, committed=16384KB)-                     Class (reserved=1066093KB, committed=14189KB)(classes #391)(malloc=9325KB #148)(mmap: reserved=1056768KB, committed=4864KB)-                    Thread (reserved=19614KB, committed=19614KB)(thread #19)(stack: reserved=19532KB, committed=19532KB)(malloc=59KB #105)(arena=22KB #38)-                      Code (reserved=249632KB, committed=2568KB)(malloc=32KB #297)(mmap: reserved=249600KB, committed=2536KB)-                        GC (reserved=10991KB, committed=10991KB)(malloc=10383KB #129)(mmap: reserved=608KB, committed=608KB)-                  Compiler (reserved=132KB, committed=132KB)(malloc=2KB #23)(arena=131KB #3)-                  Internal (reserved=9444KB, committed=9444KB)(malloc=9412KB #1373)(mmap: reserved=32KB, committed=32KB)-                    Symbol (reserved=1356KB, committed=1356KB)(malloc=900KB #65)(arena=456KB #1)-    Native Memory Tracking (reserved=38KB, committed=38KB)(malloc=3KB #41)(tracking overhead=35KB)-               Arena Chunk (reserved=237KB, committed=237KB)(malloc=237KB)

由上述执行结果可以看出,分配的16m的堆,但是NMT中确显示占用了75m的内存,这是为什么呢?

原因分析

GC部分
GC (reserved=10991KB, committed=10991KB)(malloc=10383KB #129)(mmap: reserved=608KB, committed=608KB)

由上图可知, GC malloc 分配了大约 10 MB,mmap 分配了大约 0.6 MB。如果这些结构描述了有关堆的某些内容(例如,标记位图、卡表、记忆集等),则应该可以预期它会随着堆大小的增加而增长。事实上确实如此:

# Xms/Xmx = 512 MB
-                        GC (reserved=29543KB, committed=29543KB)(malloc=10383KB #129)(mmap: reserved=19160KB, committed=19160KB)# Xms/Xmx = 4 GB
-                        GC (reserved=163627KB, committed=163627KB)(malloc=10383KB #129)(mmap: reserved=153244KB, committed=153244KB)# Xms/Xmx = 16 GB
-                        GC (reserved=623339KB, committed=623339KB)(malloc=10383KB #129)(mmap: reserved=612956KB, committed=612956KB)

有上述运行结果可知,很可能 malloc 分配的部分是并行 GC 任务队列的 C 堆分配,mmap 分配的区域是位图。毫不奇怪,它们会随着堆大小而增长,并从配置的堆大小中占用约 3-4%。这引发了部署问题,就像原始问题一样:将堆大小配置为占用所有可用物理内存将超出内存限制,可能会触发OOM内存溢出异常。

但该开销还取决于所使用的 GC,因为不同的 GC 选择以不同的方式表示 Java 堆。例如,切换回 OpenJDK 中最轻量的垃圾回收器,例如:-XX:+UseSerialGC ,在我们的测试用例中会产生以下显著变化:

-Total: reserved=1374184KB, committed=75216KB
+Total: reserved=1336541KB, committed=37573KB--                     Class (reserved=1066093KB, committed=14189KB)
+-                     Class (reserved=1056877KB, committed=4973KB)(classes #391)
-                            (malloc=9325KB #148)
+                            (malloc=109KB #127)(mmap: reserved=1056768KB, committed=4864KB)--                    Thread (reserved=19614KB, committed=19614KB)
-                            (thread #19)
-                            (stack: reserved=19532KB, committed=19532KB)
-                            (malloc=59KB #105)
-                            (arena=22KB #38)
+-                    Thread (reserved=11357KB, committed=11357KB)
+                            (thread #11)
+                            (stack: reserved=11308KB, committed=11308KB)
+                            (malloc=36KB #57)
+                            (arena=13KB #22)--                        GC (reserved=10991KB, committed=10991KB)
-                            (malloc=10383KB #129)
-                            (mmap: reserved=608KB, committed=608KB)
+-                        GC (reserved=67KB, committed=67KB)
+                            (malloc=7KB #79)
+                            (mmap: reserved=60KB, committed=60KB)--                  Internal (reserved=9444KB, committed=9444KB)
-                            (malloc=9412KB #1373)
+-                  Internal (reserved=204KB, committed=204KB)
+                            (malloc=172KB #1229)(mmap: reserved=32KB, committed=32KB)

请注意,这改进了“GC”部分,因为分配的元数据更少,也改进了“线程”部分,因为从并行(默认)切换到串行 GC 时需要的 GC 线程更少。这意味着我们可以通过调低并行、G1、CMS、Shenandoah 等的 GC 线程数来获得部分改进。我们稍后会看到线程堆栈。请注意,更改 GC 或 GC 线程数将对性能产生影响— 通过更改这一点,您将选择时依据时间复杂度和空间复杂度进权衡。

“类”部分仍然有改进的空间,因为元数据表示略有不同。我们能从“类”这个角度进一步缩减内存的占用吗?让我们尝试使用-Xshare:on启用的类数据共享 (CDS) :

-Total: reserved=1336279KB, committed=37311KB
+Total: reserved=1372715KB, committed=36763KB--                    Symbol (reserved=1356KB, committed=1356KB)
-                            (malloc=900KB #65)
-                            (arena=456KB #1)
-
+-                    Symbol (reserved=503KB, committed=503KB)
+                            (malloc=502KB #12)
+                            (arena=1KB #1)

从上述结果来看,还有有效果的。

线程部分
-                    Thread (reserved=11357KB, committed=11357KB)(thread #11)(stack: reserved=11308KB, committed=11308KB)(malloc=36KB #57)(arena=13KB #22)

从上述线程相关的内容可以看出,线程占用的大部分空间都是线程堆栈。您可以尝试使用-Xss将堆栈大小从默认值(本例中为 1M)缩减为更小的值。请注意,这会导致出现StackOverflowException 的异常的风险更大,因此如果您确实更改了此选项,请务必测试软件的所有可能配置,以防出现不良影响。大胆使用-Xss256k将其设置为 256 KB 可得到以下结果:

-Total: reserved=1372715KB, committed=36763KB
+Total: reserved=1368842KB, committed=32890KB--                    Thread (reserved=11357KB, committed=11357KB)
+-                    Thread (reserved=7517KB, committed=7517KB)(thread #11)
-                            (stack: reserved=11308KB, committed=11308KB)
+                            (stack: reserved=7468KB, committed=7468KB)(malloc=36KB #57)(arena=13KB #22)

从上述结果来看,效果还不错,在有大量线程的场景下,这种优化配置后的内存使用优化效率会更加明显,同时线程也使继Java堆后的第二大内存消耗者。

那线程是否还有优化空间呢?JIT 编译器本身也有线程。这部分解释了为什么我们将堆栈大小设置为 256 KB,但上面的数据表明平均堆栈大小仍然是7517 / 11 = 683 KB 。使用-XX:CICompilerCount=1减少编译器线程数,并设置-XX:-TieredCompilation以仅启用最新的编译层,结果如下:

-Total: reserved=1368612KB, committed=32660KB
+Total: reserved=1165843KB, committed=29571KB--                    Thread (reserved=7517KB, committed=7517KB)
-                            (thread #11)
-                            (stack: reserved=7468KB, committed=7468KB)
-                            (malloc=36KB #57)
-                            (arena=13KB #22)
+-                    Thread (reserved=4419KB, committed=4419KB)
+                            (thread #8)
+                            (stack: reserved=4384KB, committed=4384KB)
+                            (malloc=26KB #42)
+                            (arena=9KB #16)

这是预想的一样,是有效果的,内存的利用得到了进一步的优化,但是这样操作会导致编译器线程越少,预热速度越慢,从而影响应用的启动时间和线程的执行性能

减少 Java 堆大小、选择合适的 GC、减少 VM 线程数、减少 Java 堆栈线程大小和线程数是减少内存受限场景中 VM 占用空间的常用方法。

虚拟机线程栈大小

减少 VM 线程的堆栈大小是危险的,但是也值得尝试,尝试使用-XX:VMThreadStackSize=256,结果如下:

-Total: reserved=1165843KB, committed=29571KB
+Total: reserved=1163539KB, committed=27267KB--                    Thread (reserved=4419KB, committed=4419KB)
+-                    Thread (reserved=2115KB, committed=2115KB)(thread #8)
-                            (stack: reserved=4384KB, committed=4384KB)
+                            (stack: reserved=2080KB, committed=2080KB)(malloc=26KB #42)(arena=9KB #16)

2m的编译器和 GC 线程堆栈一起消失了,内存的占用得到了进一步的减小!

初始代码缓存大小(即生成代码的区域大小)

可以通过减小初始代码缓存大小(即生成代码的区域大小)可以减少内存占用吗?输入-XX:InitialCodeCacheSize=4096 (字节!),执行结果如下:

-Total: reserved=1163539KB, committed=27267KB
+Total: reserved=1163506KB, committed=25226KB--                      Code (reserved=49941KB, committed=2557KB)
+-                      Code (reserved=49941KB, committed=549KB)(malloc=21KB #257)
-                            (mmap: reserved=49920KB, committed=2536KB)
+                            (mmap: reserved=49920KB, committed=528KB)-                        GC (reserved=67KB, committed=67KB)(malloc=7KB #78)

内存的占用得到了进一步的缩减!

初始元数据存储大小

尝试设置较小的初始元数据存储大小,用 -XX:InitialBootClassLoaderMetaspaceSize=4096 (字节)将其缩减,执行结果如下:

-Total: reserved=1163506KB, committed=25226KB
+Total: reserved=1157404KB, committed=21172KB--                     Class (reserved=1056890KB, committed=4986KB)
+-                     Class (reserved=1050754KB, committed=898KB)(classes #4)
-                            (malloc=122KB #83)
-                            (mmap: reserved=1056768KB, committed=4864KB)
+                            (malloc=122KB #84)
+                            (mmap: reserved=1050632KB, committed=776KB)-                    Thread (reserved=2115KB, committed=2115KB)(thread #8)

内存的占用得到了进一步的缩减!

其它

在应用程序的数据结构设计和算法的优化上仍然有优化的空间。

综合分析

在这里插入图片描述

总结

使用 NMT 发现 VM 在哪里使用内存通常是一项很有启发性的练习。它几乎可以立即让你了解从哪里可以改善特定应用程序的内存占用。将在线 NMT 监视器连接到性能管理系统将有助于在运行实际生产应用程序时调整 JVM 参数。


http://www.ppmy.cn/ops/164884.html

相关文章

题解:AT_arc093_b [ABC092D] Grid Components

构造题。 首先,有一点很重要,构造的矩阵的两边必须小于 $100$。 所以说,我们可以先考虑构造一个上面一半白色下面一半黑色的矩形(这里直接给他弄 $100\times100$,无所谓)。 然后,如果我们白色…

线程管理操作

1.创建两个线程&#xff0c;&#xff0c;分支线程1拷贝文件的前一部分&#xff0c;分支线程2拷贝文件的后一部分 #include <head.h>#define SRC_FILE "./1.txt" #define DST_FILE "./2.txt" #define BUFFER_SIZE 4096struct copy_args {long start;l…

火语言RPA--加密PDF文件

【组件功能】&#xff1a;给PDF文件添加打开密码 配置预览 配置说明 PDF文件路径 支持T或# 默认FLOW输入项 待加密的PDF文件的完整路径。 设置密码 支持T或# 打开pdf文档输入的密码。 新文件保存文件夹 支持T或# 设置打开密码的pdf文件保存文件夹。 示例 加密PDF文件示…

修复ubuntu下找不到音频设备的问题

出现问题的状态&#xff1a; ALSA 已正确识别到 ZOOM H2n 设备&#xff08;card 1&#xff09;sounddevice 库&#xff08;依赖 PortAudio&#xff09;未能正确枚举设备 修复方法&#xff1a; 1. 强制 sounddevice 使用 ALSA 后端 默认情况下&#xff0c;sounddevice 可能尝…

Flink状态管理深度探索:从Keyed State到分布式快照

Flink状态管理深度探索:从Keyed State到分布式快照 在大数据实时计算领域,Apache Flink凭借其精准的状态管理能力成为行业标杆。本文将从状态管理的核心机制出发,结合金融行业PB级数据处理实践,深入解析状态后端、容错机制与大规模优化策略。 一、Flink状态管理核心架构 …

元脑服务器的创新应用:浪潮信息引领AI计算新时代

浪潮信息的元脑 R1 服务器现已全面支持开源框架 SGLang&#xff0c;能够在单机环境下实现 DeepSeek 671B 模型的高并发性能&#xff0c;用户并发访问量超过1000。通过对 SGLang 最新版本的深度适配&#xff0c;元脑 R1 推理服务器在运行高性能模型时&#xff0c;展现出卓越的处…

VNode

1.什么是VNode&#xff1f; VNode的全称是Virtual Node,也就是虚拟节点.它是指一个抽象的节点对象&#xff0c;用于描述真实DOM中的元素。在前端框架中&#xff0c;通过操作VNode来实现虚拟DOM&#xff0c;从而提高性能。 2.VNode的本质 本质上是JavaScript对象,这个对象就是更…

C++:vector容器(下篇)

1.vector容器机制 2.vector模拟与实现 #pragma once #include <assert.h>namespace room {template<class T>class vector{public:typedef T* iterator;// 指向数据不能修改&#xff0c;本身可以修改typedef const T* const_iterator;iterator begin(){return _st…