堆外内存持续增长最典型的信号是进程物理内存(RES)一路飙升而堆内存(-Xmx)稳定;需立即用top查RES与-Xmx差值,启用NMT(-XX:NativeMemoryTracking=detail)对比direct、internal、thread等区域增量,并用pmap -x找[anon]块,Netty应用应开启-Dio.netty.leakDetection.level=PARANOID捕获泄漏堆栈。

堆外内存持续增长,最典型的信号是进程物理内存(RES)一路飙升,而堆内存(-Xmx 设置值)却很稳定。这时候别急着调大堆,得立刻转向堆外排查。
看 RES 和堆配置的差值是否异常
用 top -p <pid> 查看 Java 进程的 RES 值,再对比你的 -Xmx(比如设了 4G)。如果 RES 达到 8G、10G 甚至更高,且随时间推移只增不降,基本可锁定堆外问题。尤其注意:即使老年代使用率(OU)平稳、Full GC 正常,RES 仍在涨——这就是堆外泄漏的典型伪装。
启用 NMT 看原生内存分配变化
必须提前加启动参数:-XX:NativeMemoryTracking=detail(上线前就该加上,临时加要重启)。然后执行:
- jcmd <pid> VM.native_memory baseline(初始快照)
- 等几小时或一天后,再执行:jcmd <pid> VM.native_memory detail.diff
重点盯这几个区域的增量:
立即学习“Java免费学习笔记(深入)”;
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- Direct:NIO DirectByteBuffer 分配量。增长几百MB甚至几个GB?大概率是 Netty 或自定义 NIO 代码没释放 ByteBuf
- Internal:JVM 内部结构开销,比如线程本地缓存、CodeCache 等。若增长显著,可能和大量线程或动态类加载有关
- Thread:线程数暴增或线程栈过大(比如 -Xss 设置过高)
- Metaspace:类加载过多未卸载,常见于热部署、OSGi、或反射生成大量代理类
用 pmap 定位匿名内存块
执行 pmap -x <pid> | sort -n -k3 | tail -20,看最后几行最大的内存段。如果出现多块几百MB甚至上GB的 [anon],且地址范围分散、无文件名,基本就是 DirectByteBuffer 或 JNI 分配的堆外内存在堆积。这类内存不会出现在 jmap 的堆转储里,所以 dump 堆内存也找不到原因。
Netty 应用要开泄漏检测开关
如果是 Netty 服务,在 JVM 参数里加上:-Dio.netty.leakDetection.level=PARANOID。它会在日志里打印出未 release 的 ByteBuf 创建堆栈,例如:
LEAK: ByteBuf.release() was not called before it's garbage-collected.
Created at: io.netty.buffer.PooledByteBufAllocator.newDirectBuffer()
顺着堆栈就能定位到哪段业务代码申请了 ByteBuf 却没调 release(),或者用了非池化缓冲区(Unpooled)但忘了释放。

















