StackTraces
Java已验证

Java OutOfMemoryError Direct buffer memory 排查

@debug_master32 天前更新阅读 6 min00

这是最容易误判的一类 OOM:堆没满、-Xmx 还有余量,进程却报内存不足。因为直接内存不在堆里,GC 根本管不到它。报错里的 used/max 是堆外缓冲池,不是 -Xmx,盯着堆调试只会越走越偏。

Issue & Env(案发现场与环境)

  • 现象:网关或调用外部接口的服务,流量上去后偶发 500,日志里出现 OutOfDirectMemoryError,随后实例健康检查异常。
  • 环境:Spring Cloud Gateway / 任何用 reactor-netty 或 NIO 的服务,底层大量使用 ByteBuffer.allocateDirect 分配堆外内存。

StackTrace(堆栈日志)

Netty 分配堆外缓冲失败时,报的是 OutOfDirectMemoryError:

io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 16777216 byte(s) of direct memory (used: 1006632967, max: 1013645312)
    at io.netty.util.internal.PlatformDependent.incrementMemoryCounter(PlatformDependent.java:667)
    at io.netty.util.internal.PlatformDependent.allocateDirectNoCleaner(PlatformDependent.java:622)
    at io.netty.buffer.PoolArena$DirectArena.allocateDirect(PoolArena.java:772)
    at io.netty.buffer.PoolArena$DirectArena.newChunk(PoolArena.java:752)
    at io.netty.buffer.PoolArena.allocateNormal(PoolArena.java:293)
    at io.netty.buffer.PoolArena.allocate(PoolArena.java:249)
    at io.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:411)

标准 JDK NIO 直接内存超限时,是另一种写法:

java.lang.OutOfMemoryError: Direct buffer memory
    at java.nio.Bits.reserveMemory(Bits.java:175)
    at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
    at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
    at org.springframework.core.io.buffer.NettyDataBufferFactory.allocateBuffer(NettyDataBufferFactory.java:157)
    at com.example.gateway.RequestCacheFilter.cache(RequestCacheFilter.java:31)
    at com.example.gateway.RequestCacheFilter.filter(RequestCacheFilter.java:24)

两行数字是定位关键:used 是已用堆外内存,max 是上限。这里 max 约 967MB,说明没有被显式放宽过。

Reproduce(复现代码)

网关里有一个 Filter 会把请求体整个 join 到一起,缓存起来给后续鉴权、日志用:

return DataBufferUtils.join(exchange.getRequest().getBody())
    .flatMap(dataBuffer -> {
        byte[] bytes = new byte[dataBuffer.readableByteCount()];
        dataBuffer.read(bytes);
        DataBufferUtils.release(dataBuffer); // 释放原始 buffer
        DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
        DataBufferUtils.retain(buffer); // 又 retain 了一个新 buffer
        return Mono.just(buffer);
    });

平时流量低看不出问题,一旦并发上来、或来了几个大请求体,直接内存就被迅速打爆。

Root Cause(根因分析)

直接内存和堆是两套账。ByteBuffer.allocateDirect 分配的内存由操作系统管理,不走 GC,-Xmx 限制不到它。JVM 对直接内存的默认上限是「跟随 -Xmx」——显式设置过 -XX:MaxDirectMemorySize 才算数。

上面这个 Filter 有三个叠加的问题:

  1. 不限制请求体大小,来多大读多大,50MB 的文件也整个 join 进内存。
  2. join 之后 retain() 出来的新 buffer,其生命周期完全依赖下游 Filter 链能否正常消费。一旦熔断超时或抛异常,没人 release,就成孤儿对象一直占着堆外内存。
  3. join 过程没有 onErrorResume 或 doFinally 兜底,连接中断时中间态的 buffer 直接泄漏。

这类泄漏在高并发下像滚雪球一样把直接内存吃光,最终网关无缓冲可用,整体不可用。

The Fix(解决方案)

先止血:显式给直接内存设一个上限,并和堆隔离:

java -XX:MaxDirectMemorySize=256m -Xmx512m -jar app.jar

默认值「跟随 -Xmx」往往偏小,单独显式设置才能真正放宽。容器里记得 MaxDirectMemorySize 加 -Xmx 之外的内存总和不能超过容器 limit,否则会被 K8s OOM Killer 干掉。

根子上要堵住泄漏:

  1. 给请求体加大小上限,超大文件不接受或走分片,别无脑 join。
  2. 对 retain() 出去的 buffer,用 doFinally 或 onErrorResume 保证异常路径也 release。
  3. 能用 heap buffer 的场合就别用 direct,减少堆外占用。

排查时不要看 -Xmx 和 GC 日志,去看 Netty 的直接内存计数、以及容器实际内存水位,定位是谁在堆外一直没释放。

References(参考来源)

最后更新于 2026-09-09

这篇帮到你了吗?

刚解决了一个棘手的报错?花两分钟记录下来,帮助下一个遇到同样问题的开发者。

贡献一条解法