29 | JVM 参数 -Xmx 设置了却还是 OOM?

摘要-Xmx 只限制堆内存,但 JVM 还用元空间、DirectBuffer、线程栈等"堆外内存"。这些区域没限制好,即使堆还有空,也会 OOM。


一、问题现象

设置了 -Xmx2g,但进程占用的内存远超 2G,最终被操作系统 OOM Killer 杀掉:

# JVM 参数
java -Xmx2g -Xms2g -jar app.jar

# 运行时观察
top -p <pid>
# RES 内存占用 3.5G(远超 2G 堆)

再看一个更直接的:

public class OffHeapOOM {
    public static void main(String[] args) {
        // 申请大量 DirectByteBuffer(堆外内存)
        List<ByteBuffer> buffers = new ArrayList<>();
        while (true) {
            buffers.add(ByteBuffer.allocateDirect(1024 * 1024 * 100)); // 每次 100MB
        }
    }
}

运行结果:

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory

堆还有大量空间,但 DirectBuffer 区域 OOM 了。


二、踩坑现场

场景 1:NIO 读取大文件,DirectBuffer OOM

// ❌ 错误:大量使用 DirectByteBuffer,不限制大小
FileChannel channel = FileChannel.open(Path.of("bigfile.zip"));
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024 * 500); // 500MB
// 多个请求同时读取大文件,DirectBuffer 累计超限

场景 2:元空间(Metaspace)OOM

// ❌ 动态生成大量类(如 CGLIB 代理),元空间 OOM
for (int i = 0; i < 100000; i++) {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(User.class);
    enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
    enhancer.create();  // 每次 create 都生成新类,占用元空间
}

运行结果:

java.lang.OutOfMemoryError: Metaspace

三、原理解析

3.1 JVM 内存布局(全貌)

进程总内存
├── 堆(Heap)← -Xmx 控制这里
│   └── 新生代 / 老年代
├── 元空间(Metaspace)← -XX:MaxMetaspaceSize 控制
│   └── Class 元数据、方法字节码、常量池
├── DirectBuffer(堆外)← -XX:MaxDirectMemorySize 控制
│   └── NIO 使用的堆外内存
├── 线程栈(Thread Stack)← -Xss 控制每个线程
│   └── 每个线程的栈空间(局部变量、方法调用链)
├── Code Cache ← -XX:ReservedCodeCacheSize 控制
│   └── JIT 编译后的本地代码
└── GC 保留空间

-Xmx 只限制「堆」的大小,其他区域不受 -Xmx 限制!

3.2 各内存区域的 OOM 类型

OOM 类型 原因 限制参数
Java heap space 堆满了 -Xmx
Metaspace 元空间满了(类太多) -XX:MaxMetaspaceSize
Direct buffer memory DirectBuffer 满了 -XX:MaxDirectMemorySize
Unable to create new native thread 线程数达到 OS 限制 ulimit -u / 减少线程数
Requested array size exceeds VM limit 申请的数组太大 拆分数据

3.3 为什么 DirectBuffer 不受 -Xmx 限制?

ByteBuffer.allocateDirect() 申请的是操作系统堆外内存(不受 JVM 堆管理):

ByteBuffer.allocateDirect(100MB)
  │
  ▼
调用 System.allocateDirectMemory()
  │
  ▼
向操作系统申请 100MB 堆外内存
  │
  ▼
JVM 记录已申请的 DirectMemory 总量
  │
  ▼
超过 MaxDirectMemorySize 时 → OOM: Direct buffer memory

默认限制:如果不设置 -XX:MaxDirectMemorySize,默认等于 -Xmx 的值(但依然是堆外,不算在堆里)。


四、正确写法

4.1 设置 Metaspace 上限

# ✅ 同时限制堆和元空间
java -Xmx2g \
     -XX:MaxMetaspaceSize=256m \
     -XX:MetaspaceSize=128m \
     -jar app.jar

4.2 设置 DirectMemory 上限

# ✅ 限制 DirectBuffer 大小
java -Xmx2g \
     -XX:MaxDirectMemorySize=512m \
     -jar app.jar

4.3 计算进程总内存公式

进程总内存 ≈ -Xmx           (堆)
             + MaxMetaspaceSize(元空间)
             + MaxDirectMemorySize(堆外)
             + 线程数 × -Xss    (线程栈)
             + ReservedCodeCacheSize(代码缓存)
             + 200~500MB        (JVM 自身开销)

# 示例(-Xmx2g):
总内存 ≈ 2g + 256m + 512m + (200线程 × 1m) + 128m + 300m
       ≈ 3.4g

4.4 监控各内存区域

# 查看堆使用
jmap -heap <pid>

# 查看元空间
jstat -gc <pid> | awk '{print "Metaspace:", $6, "KB"}'

# 查看 DirectBuffer
jcmd <pid> VM.native_memory summary

五、最佳实践

✅ JVM 内存参数设置的 5 条规范

  1. -Xmx-Xms 设置成一样大,避免运行时动态扩容触发 Full GC
  2. Metaspace 必须设置上限-XX:MaxMetaspaceSize),防止类加载泄漏
  3. 使用 NIO 的场景必须设置 -XX:MaxDirectMemorySize
  4. 线程数多的应用,减小 -Xss(默认 1M 太大,可以设成 256k)
  5. 容器化部署(Docker)必须设置 -XX:+UseContainerSupport(JDK 8u191+ 支持)

🔍 容器化部署的特殊坑

# ❌ 老版本 JDK 在 Docker 里读的是宿主机的 CPU/内存
# 导致 -Xmx 计算错误(按宿主机 64G 内存算,实际容器只有 2G)

# ✅ JDK 8u191+ / JDK 11+ 加这个参数
java -XX:+UseContainerSupport \
     -XX:MaxRAMPercentage=75.0 \
     -jar app.jar

🛠️ 阿里巴巴 Java 开发手册规约

【推荐】 服务器 8 CPU 以上,JVM 内存推荐给 4G~8G。

【推荐】 -Xmx-Xms 配置成一样,避免 GC 后堆震荡。


六、小结

  • -Xmx 只限制堆内存,元空间、DirectBuffer、线程栈都不受它限制
  • 进程总内存 = 堆 + 元空间 + DirectBuffer + 线程栈 + 代码缓存 + JVM 开销
  • Metaspace 必须设上限-XX:MaxMetaspaceSize),否则类加载泄漏会导致 OOM
  • NIO 场景设 DirectMemory 上限-XX:MaxDirectMemorySize
  • 容器化部署必须开启 -XX:+UseContainerSupport

下一篇预告:SoftReference vs WeakReference,GC 态度完全不同 —— 软引用适合做缓存,弱引用适合防止内存泄漏,两者的 GC 回收时机完全不同。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐