堆外内存泄漏的确认依据是:JVM堆内存使用率低但进程RSS持续上涨,或频繁抛出java.lang.OutOfMemoryError: Direct buffer memory;VisualVM等工具显示堆稳定而top中RES飙升,且NMT无法定位具体代码行,需借助jemalloc追踪原生调用栈。

怎么确认是堆外内存泄漏,而不是堆内存问题
堆外内存泄漏最典型的信号是:JVM堆内存(-Xmx)用得不多,但进程 RSS(常驻集大小)持续上涨,最终被系统 OOM killer 杀掉,或者 java.lang.OutOfMemoryError: Direct buffer memory 频繁抛出。
关键区别点:
-
VisualVM或jstat看堆内存(heap)稳定,但top里RES列涨得飞快 → 很可能堆外 -
DirectByteBuffer分配走的是Unsafe.allocateMemory(),不受 GC 控制,只靠Cleaner异步回收,延迟高、易遗漏 - JDK 8u20+ 默认开启
-XX:+UseG1GC后,DirectByteBuffer的清理更依赖 G1 的并发周期,一旦线程阻塞或Cleaner队列积压,就容易堆积
用 jemalloc 替换系统 malloc,为什么能定位原生分配泄漏
jemalloc 提供细粒度的内存分配追踪能力,比 glibc 的 malloc_stats() 更适合 Java 进程 —— 它能记录每次 malloc/memalign/posix_memalign 调用的调用栈,而 JVM 的堆外分配(如 DirectByteBuffer、Netty 的 Unsafe、libnet 底层 socket 缓冲区)基本都走这些接口。
实操要点:
立即学习“Java免费学习笔记(深入)”;
- 启动 JVM 前设置环境变量:
LD_PRELOAD=/path/to/libjemalloc.so,并加参数:-XX:NativeMemoryTracking=detail - 触发疑似泄漏后,用
sudo jemalloc.sh --unresolved --stacks(需提前用MALLOC_CONF=prof:true,prof_prefix:jeprof. heap启动)生成堆栈快照 - 注意:JDK 自带的
NMT(jcmd <pid> VM.native_memory summary)只能看到大类(如Internal、Other),无法定位到具体代码行;jemalloc 才能落到io.netty.util.internal.PlatformDependent0#allocateMemory这一级
VisualVM 能看堆外内存吗?怎么看才不误导
VisualVM 本身不直接显示堆外内存占用,但它可以通过插件和间接指标辅助判断。默认的「监视」页只反映堆内情况,盲目相信它显示“内存正常”会错过泄漏。
有效做法:
- 装
VisualVM-MBeans插件,查看java.nio.BufferPool.direct的TotalCapacity和Count—— 这是 JDK 暴露的唯一直接指标,但仅覆盖DirectByteBuffer,不包括 JNI、Netty native buffer、log4j2 的RingBuffer等 - 配合
jcmd <pid> VM.native_memory summary scale=MB对比:如果Internal+Other持续增长,且远超Direct池统计值,说明有非 JDK 管理的堆外分配(比如 C++ 库、自定义 JNI) - 别依赖 VisualVM 的「本机内存」图表 —— 它只是对
/proc/<pid>/status中VmRSS的采样,没有上下文,看不出增长来源
排查时最容易被忽略的三个点
堆外泄漏难,往往不是因为工具不会用,而是某些路径太隐蔽,一不留神就跳过。
- Netty 的
PooledByteBufAllocator默认启用池化,但如果配置了.directMemoryCacheAlignment(0)或禁用池(.disableCache()),会退化为大量Unsafe.allocateMemory()调用,且无统一回收入口 - Log4j2 的异步日志(
AsyncLogger)底层用Disruptor,其RingBuffer是堆外分配的,且生命周期绑定到LoggerContext,Web 应用热部署时若未正确关闭上下文,RingBuffer就永远留在那里 - JNI 代码中调用
env->NewDirectByteBuffer()后,没调用env->DeleteDirectByteBuffer(),或者 C 层用malloc分配后忘了free—— 这类泄漏完全绕过 JVM 监控体系,只能靠 jemalloc 或valgrind --tool=memcheck(需停机)
堆外内存不像堆内存有 GC 日志可查,也没有统一的引用链分析工具。每一条可疑的 Unsafe 调用、每一个第三方 native 库、每一次热部署后的资源清理,都得手动对齐。漏掉其中一环,问题就还在那里。










