nmt 必须在 jvm 启动时通过 -xx:nativememorytracking=detail 开启,运行中无法动态启用;启用后用 jcmd vm.native_memory summary/diff 定位持续增长的 native 模块(如 thread、metaspace、internal),再通过 detail 模式下 malloc/mmap 调用栈下钻至 libjvm.so 偏移定位泄漏源头。

诊断和解决 JVM 本地内存泄漏(Native Memory Leak),核心是盯住 JVM 自身在操作系统层面申请的 native memory,而不是 Java 堆。这类泄漏常表现为:进程 RSS 持续上涨、容器内存超限被 kill、系统 OOM Killer 干掉 Java 进程,但堆内存(-Xmx)始终平稳——此时堆分析工具(如 MAT)基本无效,必须转向 Native Memory Tracking(NMT)。
启用 NMT 并确认生效
NMT 必须在 JVM 启动时开启,运行中无法动态激活:
- 加启动参数:
-XX:NativeMemoryTracking=summary(轻量,看模块总量)或-XX:NativeMemoryTracking=detail(推荐,支持调用栈下钻) - 建议搭配日志输出:
-Xlog:nmt+startup+level=info(Java 10+)或-XX:+PrintNMTStatistics(旧版),确保启动成功 - 验证是否生效:执行
jcmd <pid> VM.native_memory summary</pid>,能返回各模块内存用量即表示已启用
定位持续增长的 native 模块
本地内存泄漏不体现为对象堆积,而是某类 native 分配持续增长且不回落。操作步骤如下:
- 首次快照:
jcmd <pid> VM.native_memory summary scale=MB</pid> - 等待业务稳定周期(如 5–10 分钟或一个完整请求波峰后),再次执行同命令
- 用文本工具(如 vimdiff、Beyond Compare)比对两次输出,重点关注各模块的 committed 值变化
- 重点怀疑模块:
-
Thread:线程数异常增加,或
-Xss设置过大导致单线程栈占用过高 -
Metaspace:类加载未卸载,常见于热部署、OSGi、频繁使用
Unsafe.defineClass或反射生成类 - Internal:JVM 内部结构膨胀,如 G1 的 remembered set、ZGC 的 metadata、JFR 缓冲区等
- CodeCache:JIT 编译方法过多且未清理,尤其在大量 lambda、动态代理场景下易触发
-
Thread:线程数异常增加,或
下钻到分配源头(仅 detail 模式支持)
当某个模块 committed 明显增长,需进一步定位具体分配位置:
- 执行:
jcmd <pid> VM.native_memory detail scale=KB</pid> - 关注 Virtual Memory Map 区域下的 malloc/mmap 行,例如:
malloc=42368KB #17892 calls from [libjvm.so+0x1a2b3c]
这表示该模块有近 1.8 万次 malloc 调用,总占 42MB,来自 JVM 自身代码段 - 结合地址与符号表:若需更精确来源,可配合
perf record -p <pid></pid>+perf report,或用pstack <pid></pid>查看对应线程栈,交叉比对 libjvm.so 中的偏移地址 - 典型线索:
allocation@12345 (8KB)行后的括号大小 + 频次,能判断是否为小块高频分配(如缓存、缓冲区未复用)
常见原因与对应修复
确认泄漏模块后,针对性处理:
-
Thread 泄漏:检查线程池未 shutdown、Timer/ThreadPoolExecutor 未正确关闭、第三方 SDK 创建守护线程未回收;统一使用
ThreadFactory记录线程创建点 -
Metaspace 持续增长:启用
-XX:+TraceClassLoading和-XX:+TraceClassUnloading日志,确认是否有类加载但不卸载;限制元空间上限-XX:MaxMetaspaceSize并观察是否触发 full GC 卸载 -
CodeCache 溢出:增加上限
-XX:ReservedCodeCacheSize,或启用分层编译降级(-XX:TieredStopAtLevel=1)减少 JIT 编译压力 -
Internal 类泄漏:升级 JDK 版本(部分 bug 已在新版本修复),禁用非必要功能如 JFR(
-XX:StartFlightRecording=disabled)、关闭 GC 日志滚动(-Xlog:gc*=off)











