【Java踩坑笔记】29_JVM参数-Xmx设置了却还是OOM?
·
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 条规范
-Xmx和-Xms设置成一样大,避免运行时动态扩容触发 Full GC- Metaspace 必须设置上限(
-XX:MaxMetaspaceSize),防止类加载泄漏 - 使用 NIO 的场景必须设置
-XX:MaxDirectMemorySize - 线程数多的应用,减小
-Xss(默认 1M 太大,可以设成 256k) - 容器化部署(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 回收时机完全不同。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)