排查 堆外内存泄漏是一项复杂的工作,因为堆外内存不受 JVM 垃圾回收器(GC)的直接管理4。通常可以按照以下系统化的步骤进行排查:
第一步:确认泄漏现象
监控物理内存:使用 top 或 htop 命令观察 Java 进程的 RES(常驻物理内存)指标。如果 RES 持续增长,且远超 JVM 堆内存设置(-Xmx)与元空间(-XX:MaxMetaspaceSize)之和,这通常是堆外内存泄漏的强烈信号1。
查看报错日志:注意日志中是否出现 OutOfDirectMemoryError(Netty 相关)或 OutOfMemoryError: Direct buffer memory(NIO 相关)等异常2。
第二步:定位泄漏区域(使用 NMT 工具)
JVM 自带的 Native Memory Tracking (NMT) 是排查堆外内存的核心工具,但必须在应用启动时添加参数才能生效6。
开启 NMT:在 JVM 启动参数中添加 -XX:NativeMemoryTracking=detail(生产环境建议使用 summary 以降低性能开销)1。
设定基线:应用启动并稳定运行后,执行 jcmd VM.native_memory baseline 记录当前内存状态5。
对比差异:运行一段时间后,执行 jcmd VM.native_memory summary.diff scale=MB 对比内存变化。重点关注 Direct Buffer、Internal 或 Thread 区域是否出现异常增长1。
第三步:深入追踪与代码定位
如果确认是特定区域(如 Direct Buffer)泄漏,可以通过以下方式进一步定位:
分析内存映射:使用 pmap -x | sort -rn -k3 | head -30 查看进程占用的内存段,找出占用最大且持续增长的匿名内存块(anon)2。
线程级溯源:执行 pstack 获取线程堆栈,搜索持有 java.nio.DirectByteBuffer 或 io.netty.buffer.PooledUnsafeDirectByteBuf 的线程栈,结合 NMT 的 detail 信息定位到具体分配内存的线程4。
框架专项排查(如 Netty):如果是 Netty 导致的泄漏,可在启动参数中加入 -Dio.netty.leakDetection.level=PARANOID,Netty 会在日志中记录未正确释放的 ByteBuf 的创建堆栈1。
第四步:代码审查与修复
堆外内存泄漏通常由以下代码问题引起,需重点审查:
未正确释放资源:如 NIO 的 ByteBuffer.allocateDirect() 未调用 cleaner 或显式 free();Netty 中在 ChannelHandler 处理完 ByteBuf 后未调用 release() 或未传递给下一个 Handler1。
资源未关闭:FileChannel.map() 后未调用 unmap(),或未使用 try-with-resources 正确关闭 AsynchronousSocketChannel 等流对象4。
JNI 本地内存泄漏:如果调用了 Native 方法,需检查 C/C++ 代码中是否正确释放了分配的本地内存3。
最佳实践建议:在代码中尽量使用 try-with-resources 语句,或在 finally 块中确保调用 ReferenceCountUtil.release() 等工具类来安全释放堆外资源

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐