必须显式设置-XX:MaxDirectMemorySize,不能依赖默认值;它控制ByteBuffer.allocateDirect()分配的直接内存上限,单位支持k/m/g,运行时不可修改,设为0可禁用。

直接内存上限必须显式设,不能靠默认值
OpenJDK 默认把 -XX:MaxDirectMemorySize 设为和 -Xmx 相同(JDK 8u121+),但这只是“碰巧相等”,不是合理规划。实际中堆内存可能设为 4GB,但 NIO 负载轻的服务根本用不到 4GB 堆外内存——结果就是容器里多占 4GB 物理内存,还容易触发 OOM Killer 杀进程。
必须在启动时明确加参数,例如:-XX:MaxDirectMemorySize=512m。不设就等于放任 DirectByteBuffer 无节制分配,尤其在 Netty、Kafka 客户端、Elasticsearch Transport 层等场景下非常危险。
- 设为
0可彻底禁用(适合纯同步 HTTP、无 NIO 的老系统) - 单位支持
k/m/g,不要写错成MB或mb(JVM 会静默忽略) - 该参数运行时不可改,改了要重启 JVM
怎么定这个数值才不算拍脑袋
看真实负载,不是看物理内存比例。重点参考三类数据:
-
连接数 × 单连接 buffer 估算:Netty 默认每个连接约用 64KB–256KB direct buffer;若长连接 5000 个,按 128KB 算就是 640MB,再加 20% 缓冲 → 建议设
768m -
NMT 实测值:加
-XX:NativeMemoryTracking=summary启动后,用jcmd查VM.native_memory summary Direct行的committed值,观察高峰期是否稳定在 300MB 左右?那就设400m比较安全 -
容器总内存余量:Docker 限制 2GB 内存时,
-Xmx1g + -XX:MaxDirectMemorySize=384m + -XX:MaxMetaspaceSize=256m + 线程栈(-Xss256k × 200线程 ≈ 50MB)总和 ≈ 1.68GB,留出余量防突发
Netty 和 MySQL 驱动会绕过 JVM 限制
-XX:MaxDirectMemorySize 只管 ByteBuffer.allocateDirect() 这条路径,但 Netty、MySQL Connector/J 等框架常自己管理 buffer 池,它们的配置优先级更高,必须单独处理:
- Netty:加
-Dio.netty.maxDirectMemory=536870912(单位字节),该值会覆盖 JVM 参数;同时确认PooledByteBufAllocator是否启用(默认是),否则每条连接都 new 新 buffer,上限再大也扛不住 - MySQL 驱动:检查是否开了
useDirectByteBuffer=true(Connector/J 8.0+ 默认开启),高并发查询易导致 DirectBuffer 泄漏,可临时关掉测试 - MappedByteBuffer:通过
FileChannel.map()分配的内存完全不受-XX:MaxDirectMemorySize约束,必须靠业务逻辑控制映射大小、及时调用cleaner.clean()
容器里不加 -XX:+UseContainerSupport 就白配
OpenJDK 8u191+ / JDK 10+ 默认启用该参数,但旧镜像或手动构建的 OpenJDK 可能没开。不开的话,JVM 看不到 cgroup 限制,仍按宿主机内存算默认堆和直接内存上限,极易超限被 kill。
验证方法:java -XshowSettings:vm -version 输出里看 MaxHeapSize 是否接近容器内存限制(比如 -m 2g 容器里显示 MaxHeapSize ≈ 512m 才对)。如果显示 2GB+,说明容器支持没生效。
补救方式:显式加 -XX:+UseContainerSupport,并确保 -XX:MaxDirectMemorySize 是你手动设的值,而不是依赖它自动推导——JVM 不会用容器内存自动设 DirectMemory 上限,这点很多人踩坑。
真正难控的从来不是参数本身,而是框架层 buffer 池的生命周期和 MappedByteBuffer 的 clean 时机。这两个地方一漏,-XX:MaxDirectMemorySize 再小也拦不住内存持续上涨。











