Java Native内存泄漏需用NMT detail模式定位,启动时启用并建baseline比对diff,重点排查threadinternal增长,配合pmap/strace验证第三方库泄漏。

Java Native 内存泄漏不能靠堆 dump 或 GC 日志发现,因为问题根本不在堆里。JVM 堆外本地内存持续增长,RSS 一路飙升,但 jstat 看起来一切正常,服务最终被 OOM Killer 杀掉——这时候必须用 Native Memory Tracking(NMT)从源头定位。
启动时必须开启 NMT detail 模式
NMT 不是运行时可开关的功能,错过启动就无法回溯。生产环境直接启用 detail 级别,避免 summary 模式下丢失调用栈信息:
- 参数要写在 java 命令最前面,例如:
java -XX:NativeMemoryTracking=detail -XX:+UnlockDiagnosticVMOptions -jar app.jar - 不能写成
=on或放在-jar后面,否则 JVM 静默忽略 - 搭配
-XX:+PrintNMTStatistics可确认启动时已加载,日志中会出现 “Native Memory Tracking is on” - 开销约 5–10%,但换来精准定位能力,值得长期开启
用 baseline + diff 抓住增长点
单次快照看不出问题,关键在“变化”。必须手动建立基线,再比对差异:
- 应用刚启动、负载平稳时执行:
jcmd <pid> VM.native_memory baseline</pid> - 运行一段时间(如压测后或复现泄漏场景后)执行:
jcmd <pid> VM.native_memory detail.diff scale=MB</pid> - 重点看 committed 列明显上涨的模块,尤其是:
[thread]、[class]、[internal]、[other] - 快速过滤大块变动:
jcmd <pid> VM.native_memory detail.diff scale=MB | grep -A5 -B5 "committed.*[0-9]M"</pid>
按高危区域逐个排查
不同模块增长对应不同根因,需结合业务代码和框架行为交叉验证:
立即学习“Java免费学习笔记(深入)”;
-
Thread 区域暴涨:实际线程数(
jstack <pid> | grep "java.lang.Thread" | wc -l</pid>)远超预期,常见于 ScheduledThreadPool 未 shutdown、异步任务未 await、或-Xss设置过大 - Class / Metaspace 持续上涨:动态类生成未卸载,比如 Spring AOP 代理、JSON 反序列化(Jackson/JDK 动态代理)、Groovy 脚本、热部署未清理类加载器
-
Internal 中 direct memory 显著增长:DirectByteBuffer 分配后未释放,Netty/Grpc 的 ByteBuf 未调
release(),或 Cleaner 未及时触发(尤其在 GC 压力低时) -
Other 区域上升(detail 模式可见调用栈):指向
Unsafe.allocateMemory或 JNI 调用,需检查图像处理、加密库、数据库驱动等本地库使用逻辑
配合系统命令交叉验证
NMT 只能追踪 JVM 自身分配,第三方 native 库(如 JNI、tcmalloc)不在其监控范围内。若 NMT 增长不明显但 RSS 仍暴涨,需进一步排查:
- 用
pmap -x <pid></pid>查看所有内存映射,重点关注大块[anon]区域(如 64MB 对齐的 chunk) - 对比两次 pmap 输出:
icdiff pmap-1.log pmap-2.log,找新增或膨胀的地址段 - 对可疑地址 dump 内容:
dd if=/proc/<pid>/mem bs=1 skip=$[0x7f2824000000] count=$[19396*1024] 2>/dev/null | strings | head -50</pid> - 必要时用
strace -f -e "brk,mmap,munmap" -p <pid></pid>观察底层内存系统调用频率


















