堆外内存泄漏指JVM堆外本地内存持续分配未释放,导致RSS上涨而堆监控正常,不抛OOM但被OOM Killer杀死;典型原因包括DirectByteBuffer堆积、元空间增长、线程数失控、JIT缓存膨胀及JNI泄漏,需通过NMT配合jcmd定位并针对性修复。

堆外内存泄漏是指Java应用在JVM堆以外的本地内存中持续分配却未释放,导致进程物理内存(RSS)不断上涨,而堆内存监控(如jstat、JMX)看起来完全正常。它不抛OutOfMemoryError,却悄悄耗尽系统资源,最终被Linux OOM Killer强制杀掉进程。
典型表现与常见原因
最直观的信号是:top或ps看到Java进程RSS从2GB涨到10GB,但jstat -gc显示老年代使用率始终低于40%、GC频率低且无Full GC。服务响应变慢、容器频繁重启,日志里却找不到OOM相关异常。
-
DirectByteBuffer堆积:Netty等NIO框架高频调用
allocateDirect(),但未显式清理或未触发Cleaner回收 - 元空间持续增长:Spring AOP代理、Groovy脚本、JSON反序列化等动态生成大量类,未卸载
- 线程数失控:未shutdown的ScheduledThreadPool、未关闭的Executors,每个线程默认占1MB栈空间
- JIT Code Cache膨胀:高频反射或Lambda表达式导致编译缓存打满
- JNI或本地库泄漏:C/C++代码malloc后未free,NMT无法直接追踪,需配合pstack、valgrind等工具
启用NMT进行基础监控
NMT是HotSpot JVM内置的诊断工具,无需额外依赖。关键点在于必须在启动时开启,运行时无法动态启用。
- 生产环境推荐参数:
-XX:NativeMemoryTracking=summary(开销约1–3%,适合长期开启) - 定位具体泄漏点时用:
-XX:NativeMemoryTracking=detail(需重启,开销5–10%,记录调用栈) - 必须搭配
-XX:+UnlockDiagnosticVMOptions才能使用jcmd触发命令 - 参数要放在java命令最前面,不能写成
=on或放在-jar之后,否则静默失效
用jcmd精准排查泄漏点
开启NMT后,通过jcmd获取内存快照并比对变化,是定位的核心步骤。
立即学习“Java免费学习笔记(深入)”;
- 首次检查:运行
jcmd <pid> VM.native_memory summary,重点关注Other项是否远超Java Heap - 建立基线:应用刚启动、负载平稳时执行
jcmd <pid> VM.native_memory baseline - 对比差异:过一段时间后执行
jcmd <pid> VM.native_memory detail.diff,查看committed增长大的模块 - 快速过滤:用
grep -A5 -B5 "committed.*[0-9]M"聚焦大块变动,特别留意[thread]、[class]、[direct]、[arena]
针对性修复建议
发现泄漏区域后,需结合代码和框架机制做主动干预,不能只依赖GC和Cleaner。
- Netty等支持ReferenceCounted的框架,务必调用
buffer.release()而非仅置null - 短生命周期DirectBuffer(如单次HTTP响应),考虑复用池(如PooledByteBufAllocator)
- 长时持有buffer(如共享缓存),显式调用
((DirectBuffer) buf).cleaner().clean() - 动态类加载场景,检查ClassLoader是否泄漏,确保类能被卸载
- 线程池使用后必须调用
shutdown()+awaitTermination()


















