java.lang.outofmemoryerror: direct buffer memory 表明直接内存超限,由nio分配的堆外内存超过-xx:maxdirectmemorysize(默认≈-xmx)且未及时释放所致,与堆内存无关,需通过nmt监控、限制参数及确保directbytebuffer释放来解决。

Java 应用抛出 java.lang.OutOfMemoryError: unable to create new native thread 或 OutOfMemoryError: Direct buffer memory,往往不是堆内存不够,而是本地内存(Native Memory)耗尽——这部分内存由操作系统直接管理,JVM 不通过 GC 回收,也**不体现在 -Xmx 设置中**,因此极易被忽视。
识别本地内存问题的关键信号
本地内存溢出没有统一的“堆 dump”机制,需结合现象和日志交叉判断:
-
unable to create new native thread:线程创建失败,常见于线程数超系统限制(如 Linux 默认
/proc/sys/kernel/threads-max),或每个线程栈(-Xss)过大导致总 native 内存不足; -
Direct buffer memory:NIO 使用
ByteBuffer.allocateDirect()分配堆外内存时触发,说明 Direct Memory 超过-XX:MaxDirectMemorySize(默认≈-Xmx); - 应用未明显 OOM,但 RSS(常驻内存)持续上涨、远高于 JVM 堆设定值(如
ps -o pid,rss,vsz -p <pid></pid>显示 RSS 达 4G,而 -Xmx 只有 2G); - 频繁 Full GC 却无法降低老年代使用率,且 MAT 分析未发现大对象 —— 可能是 native 层泄漏(如 JNI、Netty 的 PooledByteBufAllocator 未释放、数据库驱动未 close)。
监控本地内存的实用命令与参数
仅靠 JMX 或 VisualVM 看不到 native 内存全貌,必须结合 OS 工具和 JVM 启动参数:
- 启动时开启 Native Memory Tracking(NMT):
-XX:NativeMemoryTracking=summary(轻量)或=detail(推荐生产环境启用),然后用jcmd <pid> VM.native_memory summary</pid>查看各区域分配(Internal、Thread、Direct、Metaspace等); - 检查线程数:
cat /proc/<pid>/status | grep Threads</pid>(当前线程数),ulimit -u(用户级最大进程/线程数),cat /proc/sys/kernel/threads-max(系统级上限); - 观察 Direct Memory 使用:
jstat -gc <pid></pid>中的CCSU(Compressed Class Space Used)不相关,要看java.nio.Bits.reserveMemory报错或 NMT 中Direct区域增长; - 排查 JNI 或第三方库:若使用 Netty、Elasticsearch Client、JDBC 驱动等,确认是否启用了 native transport 或 direct buffer 池,并检查其关闭逻辑(如
PooledByteBufAllocator是否调用.close())。
常见泄漏源与修复方式
本地内存泄漏通常源于代码对资源生命周期的误判,而非单纯配置问题:
-
未释放 Direct ByteBuffer:避免手动调用
cleaner,改用 try-with-resources 或显式((DirectBuffer) buffer).cleaner().clean()(仅限必要场景);更稳妥的是复用池(如 NettyPooledByteBufAllocator)并确保 EventLoop 正确 shutdown; -
线程泄漏:检查自定义线程池是否设置合理
maxPoolSize和keepAliveTime,避免Executors.newCachedThreadPool()无限制创建;确认所有Thread.start()都有对应join()或守护线程标记; -
JNI 或驱动泄漏:升级到已知修复版本(如旧版 MySQL Connector/J 存在 native connection 泄漏);使用
-Dio.netty.noUnsafe=true临时禁用 Netty unsafe 操作以验证是否为 native bug; -
MappedByteBuffer 未清理:文件映射内存(
FileChannel.map())不会被 GC 自动释放,需反射调用sun.misc.Cleaner(JDK 9+ 推荐用Files.readAllBytes()替代大文件 mmap)。
调优与预防建议
本地内存不像堆内存那样可“一键扩容”,重点在于约束和收敛:
- 明确设置上限:
-XX:MaxDirectMemorySize=512m(防止 Direct 内存无限增长),-Xss256k(减小单线程栈,提升线程总数容限),-XX:MaxMetaspaceSize=256m(限制类元数据,间接减少 native 开销); - 在容器环境(如 Docker)中,同步限制
--ulimit nofile=65536:nofile=65536 --ulimit nproc=4096:nproc=4096,避免 JVM 超出容器 limits; - 定期采集 NMT 快照:
jcmd <pid> VM.native_memory baseline</pid>(基准),jcmd <pid> VM.native_memory summary.diff</pid>(对比差值),定位增长最快的模块; - 压测阶段强制触发
System.gc()并观察 RSS 变化 —— 若 RSS 不降,基本可判定为 native 层泄漏。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











