直接报 java.lang.outofmemoryerror: direct buffer memory 表明堆外内存耗尽,与-xmx无关,需通过vm.maxdirectmemory()、jstat、jcmd vm.native_memory及jfr定位分配源,常见于netty等nio框架未正确释放directbytebuffer。

直接报 java.lang.OutOfMemoryError: Direct buffer memory,基本可以确定是堆外内存耗尽,和堆大小(-Xmx)无关,也查不到 GC 日志里的异常痕迹——它不走 JVM 垃圾回收流程,得从 Native 层下手。
怎么看当前 Direct Memory 限制和实际用量
启动时没设 -XX:MaxDirectMemorySize,JVM 默认会取 -Xmx 值(但容器里常被 cgroup 截断,实际更低)。先确认真实上限:
- 加参数启动并打印:
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps,看日志里有没有DirectBuffer分配失败的提示 - 代码里加一行检查:
System.out.println("MaxDirectMemory: " + sun.misc.VM.maxDirectMemory() / 1024 / 1024 + " MB"); - 运行中用
jstat -gc <pid></pid>看CCSC/MU正常但 OOM 还是报 Direct buffer,基本锁定问题区域 - 更准一点:用
jcmd <pid> VM.native_memory summary</pid>(需启动时加-XX:NativeMemoryTracking=summary)
怎么定位谁在疯狂分配 DirectByteBuffer
常见黑手是 NIO 框架(Netty、Undertow)、文件通道(FileChannel.map())、图像库(OpenCV Java binding)、Kryo(unsafe mode)——不是所有 allocateDirect() 都写在你代码里。
- 全局搜
ByteBuffer.allocateDirect(),尤其注意循环、连接复用、缓冲池未 release 的地方 - Netty 用户重点查
PooledByteBufAllocator:每次buffer.release()必须成对,漏一次就泄漏一块;禁用池化可快速验证:-Dio.netty.noPreferDirect=true - 用 JFR 启动:
-XX:+FlightRecorder -XX:StartFlightRecording=duration=60s,settings=profile,jdk.NativeMemoryUsage#enabled=true,回放时过滤jdk.NativeMemoryUsage事件,直接看到每次allocateDirect的调用栈
为什么加了 -XX:MaxDirectMemorySize 还爆?
设了上限不等于安全。根本原因常是释放路径断裂或隐式清理被关掉。
-
-XX:+DisableExplicitGC会导致System.gc()失效——而某些 NIO 实现(如老版 Netty)依赖它触发Cleaner回收 DirectBuffer - 没显式
clean()或free(),只靠 GC 回收DirectByteBuffer对象,但它的Cleaner是异步的,且可能卡在 finalizer queue 里 - K8s 环境下,cgroup 内存限制可能比
-XX:MaxDirectMemorySize更早生效,kubectl top pod和cat /sys/fs/cgroup/memory/memory.usage_in_bytes要一起看 - 别盲目调大,推荐设为堆内存的 1/4~1/2(比如
-Xmx2g就配-XX:MaxDirectMemorySize=512m),避免挤占系统可用内存
最易被忽略的是:堆内对象引用着 DirectBuffer,导致 Cleaner 不触发;或者多个线程竞争同一块 buffer 释放逻辑,造成“看似 release 了,实际没清”。真要深挖,得结合 JFR 的 native memory trace + heap dump 里找 DirectByteBuffer 实例的持有链。











