登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  java教程

JVM 原生内存上涨但堆稳定,怎样用 NMT 分类定位

来源:17golang原创

时间:2026-10-07 16:21:33 414浏览 收藏

监控里最容易误判的一种组合是: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 是否同向

排查时把两个问题分开回答:

  1. NMT 的 Total committed 是否随 RSS 一起上涨?
  2. 如果上涨,主要是哪一个分类的 committed 增量在贡献?

如果 NMT 总提交量与 RSS 同向上涨,就继续在 Class、Thread、Code、GC、Compiler、Internal 等分类中找增量。如果 NMT 总提交量基本不变,而 RSS 仍上涨,说明重点很可能在 NMT 覆盖范围之外,例如 JNI 或第三方 native 库、其他 mmap、分配器碎片等,需要转向操作系统与 native 工具。

进程 RSS、NMT 总提交量与各内存分类的关系图

图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 的变化可以帮助理解地址空间布局,但不能单独作为“实际内存泄漏”的结论。

NMT baseline 与 summary.diff、reserved 和 committed 的关系图

图2:baseline 与 diff 输出之间的关系。判断实际增长时优先追踪 committed 增量。

分类增长后,下一步该查什么

NMT 分类常见含义下一步证据
Thread线程结构与线程栈等相关内存对照线程数量、线程池指标,并执行 jcmd PID Thread.print 查找持续新增的线程名
Class类元数据、Class Space 等检查类加载数量与 ClassLoader 生命周期;用 jcmd PID VM.metaspace 查看加载器与 Metaspace 细节
CodeJIT 生成代码与代码缓存判断是否仍处于编译热身阶段,结合编译日志或 CodeCache 指标观察是否趋稳
GC收集器使用的卡表、标记结构等结合当前收集器、堆配置、Region 数量和 GC 日志解释,不要直接归因于业务对象
Compiler编译器生成代码时使用的内存确认是否有持续的编译活动或动态生成代码
Internal / Other未归入前述分类的 JVM 内部结构升级到 detail 级别获取调用点,结合版本、参数和复现场景缩小范围
Native Memory TrackingNMT 自身的跟踪开销确认增长是否来自 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。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>