堆外内存泄漏不会触发outofmemoryerror,也不在gc监控范围内,但会让进程rss持续上涨,最终被linux oom killer杀掉;需通过nmt快照对比定位[direct]/[class]/[thread]等模块的committed异常增长。

堆外内存泄漏不会触发OutOfMemoryError,也不在GC监控范围内,但会让进程RSS持续上涨,最终被Linux OOM Killer杀掉。它藏得深、难察觉,排查必须绕过堆内视角,直击本地内存分配行为。
关键监控信号:先确认是不是堆外问题
看到以下组合现象,基本可以锁定堆外泄漏:
- top或ps显示Java进程RSS远高于-Xmx设置(比如Xmx=4g,RSS却涨到10g+)
- jstat -gc显示老年代使用率稳定、GC频率低、堆内存始终回落到低位
- 容器频繁OOMKilled,但JVM日志里找不到java.lang.OutOfMemoryError: Java heap space
- GC日志正常,MAT分析堆快照未发现大对象堆积或强引用链异常
启用NMT:唯一可靠的内置追踪手段
NMT(Native Memory Tracking)是HotSpot JVM原生支持的堆外内存追踪工具,必须在JVM启动时开启,运行时无法补开。
- 生产环境长期开启用:-XX:NativeMemoryTracking=summary(开销约1–3%)
- 定位具体泄漏点时用:-XX:NativeMemoryTracking=detail(需重启,开销5–10%,带调用栈)
- 参数必须放在java命令最前面,且需配合-XX:+UnlockDiagnosticVMOptions
- 错误写法示例:-XX:NativeMemoryTracking=on 或写在-jar之后 → 会静默失效
用jcmd做增量比对:找到增长最快的模块
NMT本身不报警,价值在于快照对比。标准三步法:
- 刚启动、负载平稳时执行:jcmd
VM.native_memory baseline - 运行一段时间(如30分钟或一个业务周期)后执行:jcmd
VM.native_memory detail.diff - 重点看committed值增长大的项,用grep快速聚焦:grep -A5 -B5 "committed.*[0-9]M"
- 重点关注关键词:[thread](线程栈)、[class](元空间)、[direct](DirectByteBuffer)、[arena](本地分配器)
常见泄漏点与对应动作
根据NMT输出指向的模块,采取针对性干预:
- [direct]:检查Netty等框架是否漏调release();确认是否有未关闭的Channel或EventLoopGroup;限制Direct内存上限(-XX:MaxDirectMemorySize)并监控JMX指标
- [class]:排查动态代理(Spring AOP/CGLIB)、Groovy脚本、JSON反序列化生成类;检查ClassLoader是否可被回收;必要时增加-XX:MetaspaceSize
- [thread]:检查ScheduledThreadPool、Executors.newCachedThreadPool未shutdown;确认线程创建是否受控;观察每个线程默认1MB栈空间的累积效应
- [arena]或无明确归属:怀疑JNI调用或C/C++本地库泄漏,需结合pstack、perf record或valgrind进一步分析










