当前位置:首页 > 文章列表 > 文章 > java教程 > JVM 原生内存上涨但堆稳定,怎样用 NMT 分类定位

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

来源:17golang原创 2026-10-07 16:21:33 0浏览 收藏

监控里最容易误判的一种组合是: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删除
io.Reader 为什么允许同时返回数据和 EOF,调用方应怎样处理io.Reader 为什么允许同时返回数据和 EOF,调用方应怎样处理
上一篇
io.Reader 为什么允许同时返回数据和 EOF,调用方应怎样处理
Python import 很慢怎么分析:模块级副作用与延迟导入
下一篇
Python import 很慢怎么分析:模块级副作用与延迟导入
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    365次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    422次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    436次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    387次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    214次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码