堆外内存无固定比例,需显式配置-XX:MaxDirectMemorySize并与容器limit对齐;其不参与GC但计入RSS,易致OOMKilled;排查需结合VmRSS、native_memory及cgroup指标。

堆外内存和堆内存没有固定比例可套用。强行按“3:7”或“1:4”去配,反而容易在容器里被 OOMKilled——因为堆外内存不进 GC 统计,但会算进容器 RSS;而堆内存再小,只要堆外缓冲区疯长,照样爆 limit。
堆外内存到底占多少,得看 NIO 缓冲区和 DirectByteBuffer 分配行为
绝大多数堆外内存来自 ByteBuffer.allocateDirect(),它背后调的是 Unsafe.allocateMemory(),分配的是操作系统原生内存。关键点在于:这些缓冲区的生命周期不由 GC 管,而是靠 Cleaner 或者显式 buffer.clear() + System.gc()(不推荐)来释放。一旦应用频繁创建未复用的 direct buffer,或者用了没关的 FileChannel.map(),堆外内存就只增不减。
- 典型高风险场景:
Netty的PooledByteBufAllocator配置不当、gRPC的maxMessageSize过大导致单次接收缓冲暴涨、Spring WebFlux 中DataBuffer未及时release() - 一个
allocateDirect(1MB)创建的对象,堆上只占约 24 字节(对象头+字段),但实际占用 1MB 堆外空间——监控里完全看不到这 1MB,直到cat /sys/fs/cgroup/memory/memory.usage_in_bytes突然飙高 - JVM 默认对堆外内存的总上限由
-XX:MaxDirectMemorySize控制,不设时等于-Xmx值;但它只是 soft limit,超了不会立即 OOM,而是触发System.gc()尝试回收 Cleaner 队列,失败后才抛OutOfMemoryError: Direct buffer memory
容器环境下必须把 -XX:MaxDirectMemorySize 和 cgroup limit 对齐
在 K8s 里,如果你设了 resources.limits.memory: 4Gi,JVM 却默认把 -XX:MaxDirectMemorySize 设成 4GB(等于 -Xmx),那堆(比如 2GB)+ 堆外(比如 1.8GB)+ 线程栈 + 元空间 + 本地代码开销 ≈ 超过 4GB,OOMKilled 就是分分钟的事。
- 安全做法是显式压低堆外上限:比如容器 limit 是 4Gi,堆设
-Xmx2g,那就必须加-XX:MaxDirectMemorySize=1g,给元空间、线程栈、JIT 缓存等留出至少 512MB 余量 - 别依赖
-XX:+UseContainerSupport自动推导MaxDirectMemorySize—— 它只影响堆,默认不改堆外限制,JDK 17 之前尤其如此 - 验证是否生效:启动后查
jinfo -flag MaxDirectMemorySize,确保输出值是你设的数字,不是-1(表示未设置)或等于Xmx
怎么判断当前堆外内存是不是真瓶颈
光看 jstat -gc 没用,它不统计堆外。得结合三类指标交叉定位:
- 进程级:
cat /proc/(RSS)减去/status | grep VmRSS jstat -gc(老年代已用 + 新生代已用)≈ 当前堆外 + 栈 + 元空间等开销| awk '{print $10}' - 堆外专项:
jcmd,重点关注VM.native_memory summary Internal和Direct两栏;如果Direct持续上涨且mapped也涨,大概率是FileChannel.map()或 Netty 内存池泄漏 - GC 日志里出现
java.lang.OutOfMemoryError: Direct buffer memory,说明MaxDirectMemorySize已触顶,但更常见的是根本没报错就被 OOMKilled,此时只能靠/sys/fs/cgroup/...反推
真正难的不是算比例,而是让每个 allocateDirect() 调用都有明确的 owner、lifetime 和 release 路径。很多团队花一周调 NewRatio,却放任一个日志框架每秒 new 出 100 个 64KB direct buffer——这种地方不堵住,再准的堆外比例也是纸糊的。











