堆外内存泄漏需聚焦Direct Buffer、Metaspace、线程栈等区域:Direct Buffer未调用release()导致内存不归还;Metaspace因动态代理或ClassLoader泄漏持续增长;NMT、JMX、Arthas和RSS监控可交叉验证定位。

堆外内存泄漏不像堆内存那样有成熟的 GC 日志和 dump 机制,排查时需聚焦 Direct Buffer、Metaspace、线程栈等本地内存区域,结合监控、指标与代码逻辑交叉验证。
重点盯住 Direct Buffer 使用量
Netty、NIO Channel 等大量使用堆外 DirectByteBuffer,其生命周期靠引用计数管理。未调用 release() 就会导致内存无法归还 OS。
- 通过 JMX 获取实时用量:
java.nio:type=BufferPool,name=direct中的MemoryUsed和TotalCapacity - 在 Spring Boot 项目中可封装定时监控 Bean,每 5 秒采集一次并上报到 Prometheus 或打日志
- 若发现
MemoryUsed持续上涨且不回落,尤其在高并发接口调用后未恢复,基本可锁定为 Direct Buffer 泄漏 - 检查业务代码是否在 Netty ChannelHandler 中持有 ByteBuf 引用、是否在异常分支遗漏 release、是否用
retain()后没配对release()
排查 Metaspace 是否持续膨胀
Java 8+ 中类元数据存于本地内存,溢出提示 OutOfMemoryError: Metaspace,常见于动态代理、反射生成类场景。
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 启动参数加
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps,观察 Metaspace GC 是否频繁但回收量极少 - 用
jstat -gc <pid>查看MU(Metaspace Used)和MC(Metaspace Capacity)是否单向增长 - 搜索代码中高频调用
Class.forName()、defineClass()、CGLIB 的Enhancer.create(),尤其是循环内或请求路径中 - 确认 ClassLoader 是否被静态 Map、ThreadLocal 或监听器意外持有——这是类卸载失败的根本原因
用 Arthas 快速定位堆外增长源头
无需重启,适合生产环境初步筛查。
立即学习“Java免费学习笔记(深入)”;
- 执行
memory命令,多次刷新看non-heap或metaspace区域是否持续攀升 - 若怀疑是线程相关堆外开销(如 JNI、自定义 native buffer),用
thread -n 10查看活跃线程数是否异常增多 - 结合
sc -d *YourHandler*查看可疑类的加载器,再用classloader -t看类加载器树,判断是否存在泄漏的 ClassLoader 实例 - 对高频接口做
watch,例如监控io.netty.buffer.PooledByteBufAllocator.newDirectBuffer调用次数与返回对象,辅助判断分配节奏是否匹配业务流量
补充手段:系统级与容器视角
堆外内存最终体现为进程 RSS 增长,需跳出 JVM 看整体。
- Docker 容器中用
cat /sys/fs/cgroup/memory/memory.usage_in_bytes对比 JVM 堆上限,确认是否超出(如堆设 4G,RSS 达 12G,大概率是堆外问题) - 用
pstack <pid>或jstack <pid>查看线程栈深度和数量,过深栈或过多线程会吃掉大量pthread栈空间(默认 1MB/线程) - 检查是否有 JNI 库未正确释放 native 内存,特别是图像处理、加密解密、数据库驱动等第三方组件
- 启用 Native Memory Tracking(NMT):启动加
-XX:NativeMemoryTracking=detail,运行时用jcmd <pid> VM.native_memory summary查各模块内存分布

















