JVM 原生内存上涨但堆稳定,怎样用 NMT 分类定位
监控里最容易误判的一种组合是:Java 堆使用量和 GC 后低水位都很稳定,但操作系统看到的进程 RSS 仍在缓慢上涨。此时继续反复抓堆转储,往往只能证明“堆里没有明显增长”,却解释不了 JVM 进程为何变大。
Native Memory Tracking(NMT)的价值,就是把 HotSpot/JVM 内部的原生内存按子系统分类。它不是整个进程的内存剖析器,也不会覆盖第三方 native 代码和 JDK 类库的所有原生分配。正确用法不是盯一次快照,而是先建立基线,再在相同负载窗口比较分类增量。
本文依据 Oracle Java 25 的 Native Memory Tracking 与 jcmd 文档整理。官方地址也可直接保存:https://docs.oracle.com/en/java/javase/25/vm/native-memory-tracking.html。
先说结论:看 committed 增量,再看 NMT 与 RSS 是否同向
排查时把两个问题分开回答:
- NMT 的
Total committed是否随 RSS 一起上涨? - 如果上涨,主要是哪一个分类的
committed增量在贡献?
如果 NMT 总提交量与 RSS 同向上涨,就继续在 Class、Thread、Code、GC、Compiler、Internal 等分类中找增量。如果 NMT 总提交量基本不变,而 RSS 仍上涨,说明重点很可能在 NMT 覆盖范围之外,例如 JNI 或第三方 native 库、其他 mmap、分配器碎片等,需要转向操作系统与 native 工具。

图1:进程 RSS、NMT 总提交量和分类视角的关系。NMT 只能解释 HotSpot/JVM 内部被跟踪的部分。
NMT 为什么不能在故障发生后临时补开
NMT 默认关闭,必须在 JVM 启动时通过 -XX:NativeMemoryTracking 选择 summary 或 detail。运行中可以用 jcmd 关闭,但不能再启动或重新启动。因此,线上若经常遇到堆外增长而又缺少证据,应该把 NMT 作为诊断配置预先放进可控实例,而不是等 RSS 上涨后才想开启。
Oracle 文档给出的性能开销约为 5% 到 10%。常态诊断可先用 summary,它按子系统聚合,信息足够回答“哪类在涨”;只有需要调用点和虚拟内存映射时,才在可接受开销的实例上使用 detail。
# 启动时开启摘要级 NMT;该能力不能在运行中补开 java -XX:NativeMemoryTracking=summary -jar app.jar # 若需要调用点与虚拟内存映射,改为 detail 后重启实例 java -XX:NativeMemoryTracking=detail -jar app.jar
如果使用容器,还要先确认拿到的是容器内可见的 Java PID,并在同一主机、相同有效用户与用户组下执行 jcmd。先用目标 JVM 的帮助输出来确认命令可用,避免把权限或 PID 问题误判成 NMT 失效。
# 列出当前环境可见的 JVM 进程 jcmd -l # 查看目标 JVM 支持的 VM.native_memory 参数 jcmd PID help VM.native_memory # 读取当前摘要,并统一按 MB 展示 jcmd PID VM.native_memory summary scale=MB
在预热完成后建立基线
启动初期会发生类加载、JIT 编译、线程池扩容和缓存预热,这些增长往往是正常初始化。如果一启动就做 baseline,后续差异会把大量预热噪声算进去。更稳妥的做法是:等实例进入稳定流量、核心类已加载、主要线程池已建立后,再记录操作系统 RSS、堆使用量与 NMT 基线时刻。
# 在应用预热完成且负载稳定后建立比较基线 jcmd PID VM.native_memory baseline # 经过固定观察窗口后读取按分类汇总的差异 jcmd PID VM.native_memory summary.diff scale=MB
观察窗口应尽量保持流量、并发、请求类型和功能开关一致。比起“十分钟后又执行一次”,更可靠的描述是“同样压测脚本、同样并发、同样持续时间后执行”。这样得到的分类增量才具有可比性。
reserved 和 committed 不要混着看
reserved 表示 JVM 预留的虚拟地址空间,不等于已经占用同等大小的物理内存。例如堆上限很大时,Java Heap 的 reserved 可以很高,但实际 committed 仍较小。排查 RSS 增长时,主线应是 committed 以及差异后的正增量。
summary.diff 会在分类旁显示相对基线的变化。先看 Total committed,再按 committed 增量从大到小排序;reserved 的变化可以帮助理解地址空间布局,但不能单独作为“实际内存泄漏”的结论。

图2:baseline 与 diff 输出之间的关系。判断实际增长时优先追踪 committed 增量。
分类增长后,下一步该查什么
| NMT 分类 | 常见含义 | 下一步证据 |
|---|---|---|
| Thread | 线程结构与线程栈等相关内存 | 对照线程数量、线程池指标,并执行 jcmd PID Thread.print 查找持续新增的线程名 |
| Class | 类元数据、Class Space 等 | 检查类加载数量与 ClassLoader 生命周期;用 jcmd PID VM.metaspace 查看加载器与 Metaspace 细节 |
| Code | JIT 生成代码与代码缓存 | 判断是否仍处于编译热身阶段,结合编译日志或 CodeCache 指标观察是否趋稳 |
| GC | 收集器使用的卡表、标记结构等 | 结合当前收集器、堆配置、Region 数量和 GC 日志解释,不要直接归因于业务对象 |
| Compiler | 编译器生成代码时使用的内存 | 确认是否有持续的编译活动或动态生成代码 |
| Internal / Other | 未归入前述分类的 JVM 内部结构 | 升级到 detail 级别获取调用点,结合版本、参数和复现场景缩小范围 |
| Native Memory Tracking | NMT 自身的跟踪开销 | 确认增长是否来自 detail 级跟踪本身,评估是否需要缩短采样窗口 |
这些映射是“下一步证据”的入口,不是看到分类名就能直接判定根因。例如 Thread 上涨可能是线程泄漏,也可能只是流量上升后线程池按设计扩容;Class 上涨可能是 ClassLoader 泄漏,也可能是延迟加载。关键是看它是否在相同负载和足够长时间内持续单调增长,并与对应数量指标一致。
什么时候从 summary 升级到 detail
summary 已经能告诉你是哪类增长,但对于 Internal、Other 或大分类内部的具体来源,常常还不够。如果问题可稳定复现,可以在隔离实例或可控生产实例上以 detail 级别重启,同样先预热、建立 baseline,再查看 detail.diff。detail 会增加按调用点聚合的信息与虚拟内存映射,因此更适合做第二阶段下钻。
# detail 级实例预热完成后重新建立基线 jcmd PID VM.native_memory baseline # 在同一负载窗口后查看调用点级差异 jcmd PID VM.native_memory detail.diff scale=MB
不要在 summary 模式下期待完整调用点,也不要用 detail 的一次快照代替差异比较。真正有价值的是“哪个调用点相对稳定基线持续增加”,再把它和配置变更、代码路径、请求样本或线程栈对应起来。
NMT 总量稳定但 RSS 仍涨,意味着什么
这是 NMT 排查中最重要的分叉。Oracle 明确说明,NMT 跟踪的是 JVM/HotSpot 内部内存,不跟踪第三方 native 代码,也不覆盖 JDK 类库的所有原生分配。若同一时间窗内 NMT Total committed 基本稳定,而进程 RSS 明显增长,就不应继续在 NMT 分类里“硬找答案”。
下一步应把怀疑范围转向 JNI、压缩或加密库、图像处理库、网络库、直接调用 malloc 的 native 组件、额外 mmap 以及 native 分配器碎片。Linux 上可结合进程内存映射、匿名映射变化、pmap、/proc/PID/smaps_rollup、分配器统计和 native profiler;具体工具要与发行版、libc 和部署权限匹配。
一份可执行的最小核对单
- 确认实例启动时已开启
NativeMemoryTracking=summary或detail。 - 等预热结束后建立 baseline,而不是刚启动就采样。
- 保证前后窗口的流量、并发、持续时间和功能开关可比。
- 先看
Total committed,再看各分类 committed 增量。 - 将 Thread、Class、Code、GC 等分类增量与对应数量指标交叉验证。
- summary 只能定类,确需调用点时再以 detail 级别重启并做
detail.diff。 - NMT 总量与 RSS 不同向时,立即检查 NMT 覆盖范围之外的 native 分配。
因此,“堆稳定但 RSS 上涨”并不等于“JVM 堆泄漏”,也不能仅凭一次 NMT 快照认定 native 泄漏。最短路径是用 baseline 和 diff 把增长变成分类增量,再用 NMT 与 RSS 的差额决定是否转向系统级工具。这样既能避免在堆里反复寻找不存在的对象,也能避免把第三方原生分配错误归因给 HotSpot。
io.Reader 为什么允许同时返回数据和 EOF,调用方应怎样处理
- 上一篇
- io.Reader 为什么允许同时返回数据和 EOF,调用方应怎样处理
- 下一篇
- Python import 很慢怎么分析:模块级副作用与延迟导入
-
- 文章 · java教程 | 3小时前 | Java · 性能优化 · Stream · Java教程 · Java Stream Spliterator 并行流 parallelStream Stream副作用
- Stream 并行化前先判断什么:数据规模、拆分与副作用
- 405浏览 收藏
-
- 文章 · java教程 | 5小时前 | Java教程 · sealed interface Java密封类 模式匹配switch 支付结果 穷尽检查
- 密封类建模支付结果:穷尽分支与扩展边界
- 270浏览 收藏
-
- 文章 · java教程 | 7小时前 | 数据校验 · api设计 · Java教程 · 参数校验 Java record API DTO Jakarta Validation 紧凑规范构造器 跨字段校验
- Java Record 作为 API DTO 时,校验逻辑放在哪里
- 370浏览 收藏
-
- 文章 · java教程 | 9小时前 | 线程池 · 异常处理 · 并发编程 · Java教程 · CompletableFuture · 异步任务 completablefuture allOf Handle 结果汇总 CompletionException
- CompletableFuture 组合独立任务:allOf 结果汇总与失败归属
- 482浏览 收藏
-
- 文章 · java教程 | 11小时前 |
- StructuredTaskScope 如何表达并发任务的共同生命周期
- 425浏览 收藏
-
- 文章 · java教程 | 17小时前 | 并发编程 · Java教程 · java arena MemorySegment WrongThreadException FFM API
- Java MemorySegment 怎么限制跨线程访问范围
- 132浏览 收藏
-
- 文章 · java教程 | 19小时前 | Java · Java 24 Java Class-File API CodeTransform ClassTransform CodeAttribute
- Java Class-File API 怎么转换方法代码属性
- 199浏览 收藏
-
- 文章 · java教程 | 21小时前 | Java · Stream · java Stream Gatherer Integrator.Greedy
- Java Gatherer Integrator.Greedy 什么时候可以声明贪婪处理
- 112浏览 收藏
-
- 文章 · java教程 | 1天前 |
- Java FileChannel transferTo 为什么可能只传输部分字节
- 229浏览 收藏
-
- 文章 · java教程 | 1天前 | Java · 异步编程 · Java HttpClient BodyHandlers.fromLineSubscriber Flow.Subscriber 异步响应 按行消费
- Java HttpClient 怎么把响应体按行异步消费
- 433浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 365次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 422次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 436次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 387次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 214次使用
-
- Go Java 算法之字符串解码示例详解
- 2023-01-07 479浏览
-
- Go Java算法之单词搜索示例详解
- 2022-12-30 337浏览
-
- Gojava算法之括号生成示例详解
- 2023-02-22 128浏览
-
- GoJava算法之累加数示例详解
- 2023-01-07 149浏览
-
- GoJava算法最大单词长度乘积示例详解
- 2023-01-12 202浏览

