NMT需启动时配置-XX:NativeMemoryTracking=detail和-XX:+UnlockDiagnosticVMOptions,通过baseline+diff分析Internal、Thread、Metaspace、Other四模块committed值变化定位堆外内存问题。

直接看 NMT 输出里的 committed 值变化,重点盯 Internal、Thread、Metaspace、Other 四块——它们代表 JVM 自己向操作系统申请的非堆内存,和 Java 堆无关,也不受 -Xmx 控制。
启动时必须配对开启两个参数
NMT 不是运行时能加的功能,必须在 JVM 启动时就配置好,缺一不可:
- -XX:NativeMemoryTracking=detail:生产排查首选,能显示 DirectByteBuffer 分配栈、Unsafe 调用点、JNI 函数名等具体来源;summary 模式只给大类汇总,无法定位代码行
- -XX:+UnlockDiagnosticVMOptions:必需项,否则 jcmd 调用 VM.native_memory 会报错“not supported”或“Unknown VM option”
- 可选加 -XX:+PrintNMTStatistics:启动后立刻打印初始内存分布,方便确认是否生效
用 baseline + diff 看真实增长
单次快照容易把 JVM 启动开销当成泄漏。正确做法是分三步:
- 服务刚就绪、业务未触发时执行:jcmd <PID> VM.native_memory baseline
- 运行一段时间(如压测 15 分钟)或复现问题后,执行:jcmd <PID> VM.native_memory summary.diff scale=MB
- 重点关注 diff 中 committed 持续上涨的模块:Internal(含 DirectBuffer)、Thread(线程数异常)、Other(Unsafe/JNI)、Metaspace(类加载器泄漏)
四大关键区域怎么看、怎么查
从 NMT 输出里快速识别风险点:
-
Internal:若 committed 持续涨且 detail 模式下看到大量 java.nio.DirectByteBuffer 调用栈,检查是否漏调
cleaner().clean()或 Netty/Grpc 的 ByteBuf 未释放 -
Thread:对比
jstack PID | grep "java.lang.Thread" | wc -l和 NMT 中 Thread.committed 值;若 committed 接近 reserved,说明线程栈没回收,常见于线程池未 shutdown - Metaspace:Class 区域 committed 缓慢爬升,结合 jstat -gc PID 看 loaded 类数量是否持续增加,排查 Spring Boot 热部署、OSGi 或 defineClass 泄漏
- Other:detail 模式下出现 sun.misc.Unsafe 或 JNI 库名(如 libmycrypto.so),说明有自定义 native 内存操作,需审查 C++ 侧 malloc 是否配对 free
别被假象带偏的几个注意点
NMT 很强大,但边界明确,用错地方反而浪费时间:
- 它不监控第三方 native 库(如 OpenSSL、OpenCV、Netty 底层 epoll 实现)的内存分配,这些完全逃逸在 NMT 视野外
- Java Heap 行的 committed 是堆实际占用,和堆外无关;别把它和进程 RES 混淆
- Internal 的 malloc 分配默认不带完整 Java 调用栈,所以要确保 JNI so 文件编译时未 strip 符号,否则调用栈里看不到函数名
- detail 模式有 5–10% 吞吐量下降,只建议在复现环境或低峰期临时启用,不要长期开着

















