NMT排查堆外内存泄漏需启动时配置detail模式与UnlockDiagnosticVMOptions,通过baseline→运行→diff三步法聚焦Internal、Thread、Metaspace、Other四区增长,并结合系统命令交叉验证。

直接用 NMT 排查堆外内存泄漏,核心不是“看一次快照”,而是建立变化视角、聚焦高危模块、交叉验证来源。它不依赖 GC,也不分析对象引用,而是从 JVM 内部原生内存申请行为出发,精准定位谁在操作系统层面悄悄占内存。
启动必须配对:detail + UnlockDiagnosticVMOptions
NMT 不是运行时动态开启的功能,必须在 JVM 启动时就声明。缺一不可:
- -XX:NativeMemoryTracking=detail:生产排查唯一推荐模式。能显示 DirectByteBuffer 分配栈、Unsafe 调用位置、JNI 模块名及行号;summary 模式只分大类,无法定位到具体代码
- -XX:+UnlockDiagnosticVMOptions:否则 jcmd 调用 VM.native_memory 会报错 “not supported”,这个开关常被遗漏
- 可加 -XX:+PrintNMTStatistics:启动后立刻打印初始内存分布,5 秒内确认是否生效
三步快照法:baseline → 运行 → diff 找真增长
单次 summary 或 detail 输出意义有限,关键在“变化量”。建议按节奏操作:
- 应用刚启动、无业务流量时执行:jcmd
VM.native_memory baseline (记录基线) - 运行 15–30 分钟,或复现可疑场景(如批量导出、连接激增)后执行:jcmd
VM.native_memory summary.diff scale=MB - 重点盯输出中 Internal、Thread、Metaspace、Other 四个区域的 committed 增长值——它们覆盖了 90% 的堆外泄漏路径
四大高危区逐个盯防与验证方式
NMT 的 diff 输出里,以下四类增长需立即结合业务逻辑和系统命令交叉验证:
- Internal 区域持续上涨:重点关注 “direct memory” 字段。若与 ByteBuffer.allocateDirect() 调用量强相关,检查是否漏调 Cleaner、Netty 的 release()、或未关闭 Grpc 的 ByteBuf
-
Thread 区域 committed 接近 reserved:用 jstack
| grep 'java.lang.Thread' | wc -l 查真实线程数,再对比 NMT 中 Thread 的 #xxx 数值。若线程数稳定但 committed 持续涨,说明栈内存未释放,常见于线程池未 shutdown、CompletableFuture 未 join、异步任务未 await -
Metaspace 的 Class committed 缓慢爬升:尤其在 Spring Boot DevTools、OSGi、或使用 ASM/CGLIB 动态生成类的场景。配合 jstat -gc
看 MC/MU 是否同步增长,再用 jcmd VM.class_hierarchy 查加载器数量是否异常增多 -
Other 区域在 detail 模式下出现 Unsafe.allocateMemory 或 JNI 调用栈:说明有本地库或自定义 native 方法在分配内存。此时可用 pmap -x
查大内存块地址,再用 cat /proc/ /smaps | grep -A 5 "your_lib_name" 定位归属
不能只信 NMT:必须用系统命令交叉印证
NMT 只统计 JVM 自己申请的内存,不包含第三方 native 库(如 OpenSSL、FFmpeg)的 malloc。因此发现 RSS 持续上涨但 NMT 增长不匹配时,要补查:
-
ps -o pid,rss,vsz,comm -p
:看进程总物理内存(RSS)是否确实在涨 -
pmap -x
| tail -20 :找 top 20 大内存映射段,看是否有非 JVM 的 .so 或匿名映射 -
cat /proc/
/smaps | awk '/^Size:/ {sum+=$2} END {print sum}' :算总提交内存,与 NMT 的 Total committed 对比,差值超过 20% 就要怀疑外部 native 泄漏










