常见的OOM错误 ( OutOfMemoryError全类型详解)

📊 OOM 常见类型总览

错误类型 触发区域 根本原因 常见场景
Java heap space 堆内存 对象太多,堆放不下 内存泄露、大对象、流量突增
GC overhead limit exceeded 堆内存 GC效率低下 内存泄露、堆太小、代码问题
Metaspace 元空间 类加载太多 动态代理、反射、热部署
Direct buffer memory 直接内存 堆外内存不足 NIO、Netty、MMAP
Unable to create new native thread 栈/系统 线程太多 线程池配置不当、递归过深
Requested array size exceeds VM limit 堆内存 数组过大 大数组创建
Kill process or sacrifice child 系统 系统内存不足 容器限制、物理内存不足

🔍 1. Java heap space (最常见)

错误信息:

复制代码
java.lang.OutOfMemoryError: Java heap space

核心原因:

对象实例占满了整个堆内存,且无法被GC回收

典型场景:

java 复制代码
// 场景1:内存泄露 - 静态集合类持有引用
public class MemoryLeak {
    private static final List<byte[]> LIST = new ArrayList<>();
    
    public void addData() {
        while (true) {
            LIST.add(new byte[1024 * 1024]);  // 1MB
        }
    }
}

// 场景2:大对象处理
public class BigObject {
    public void processLargeFile() {
        // 一次性读取大文件到内存
        byte[] fileContent = Files.readAllBytes(Paths.get("huge_file.bin"));  // 10GB文件
    }
}

// 场景3:缓存失控
public class CacheOOM {
    private Map<String, String> cache = new HashMap<>();
    
    public void loadDataToCache() {
        // 从数据库加载大量数据到内存
        List<User> users = userDao.findAll();  // 百万条记录
        for (User user : users) {
            cache.put(user.getId(), user.serialize());
        }
    }
}

排查步骤:

bash 复制代码
# 1. 查看当前堆内存使用
jmap -heap <pid>

# 2. 生成堆转储文件
jmap -dump:format=b,file=heap.hprof <pid>

# 3. 实时监控GC
jstat -gc <pid> 1000  # 每秒打印一次

# 4. 使用jcmd
jcmd <pid> GC.heap_info

解决方案:

java 复制代码
// 1. 合理设置JVM参数
// 生产环境示例
-Xms4g -Xmx4g  // 堆内存4G,避免动态扩展
-XX:+UseG1GC   // 使用G1垃圾收集器
-XX:MaxGCPauseMillis=200  // 目标暂停时间
-XX:+HeapDumpOnOutOfMemoryError  // OOM时自动dump
-XX:HeapDumpPath=/path/to/dumps

// 2. 修复内存泄露代码
public class FixedMemoryLeak {
    // 使用弱引用或软引用
    private static final Map<String, SoftReference<BigObject>> CACHE = new WeakHashMap<>();
    
    // 或使用LRU缓存
    private static final Map<String, BigObject> safeCache = 
        Collections.synchronizedMap(new LinkedHashMap<String, BigObject>(16, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest) {
                return size() > 1000;  // 限制大小
            }
        });
}

// 3. 分批处理大数据
public class BatchProcessor {
    public void processLargeData() {
        int batchSize = 1000;
        int offset = 0;
        
        while (true) {
            List<Data> batch = dao.findBatch(offset, batchSize);
            if (batch.isEmpty()) break;
            
            processBatch(batch);
            offset += batchSize;
            
            // 提示GC,但不是强制
            if (offset % 10000 == 0) {
                System.gc();
            }
        }
    }
}

⏳ 2. GC overhead limit exceeded

错误信息:

复制代码
java.lang.OutOfMemoryError: GC overhead limit exceeded

核心原因:

JVM花费了98%以上的时间进行GC,但只回收了不到2%的堆内存

触发条件(默认):

  • GC时间占比超过98%
  • 回收的内存不到堆的2%
  • 连续5次GC都满足上述条件

典型场景:

java 复制代码
// 场景1:字符串拼接在循环中
public class StringOOM {
    public String buildHugeString() {
        String result = "";
        for (int i = 0; i < 1000000; i++) {
            result += "some data ";  // 每次创建新StringBuilder和String
        }
        return result;
    }
}

// 场景2:频繁创建临时对象
public class TempObjectOOM {
    public void process() {
        while (true) {
            // 每次循环都创建新对象,快速进入老年代
            byte[] buffer = new byte[1024 * 1024];  // 1MB
            // 但buffer很快失去引用,成为垃圾
        }
    }
}

排查方法:

bash 复制代码
# 1. 查看GC详细日志
java -Xlog:gc*,gc+heap=debug:file=gc.log -Xmx512m YourApp

# 2. 使用VisualGC或JConsole监控
# 3. 分析GC日志文件

解决方案:

java 复制代码
// 1. 优化JVM参数
-Xmx2g -Xms2g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1ReservePercent=15
-XX:InitiatingHeapOccupancyPercent=35

// 2. 代码优化
public class OptimizedStringBuilder {
    public String buildHugeString() {
        StringBuilder sb = new StringBuilder(10000000);  // 预分配容量
        for (int i = 0; i < 1000000; i++) {
            sb.append("some data ");
        }
        return sb.toString();
    }
}

// 3. 对象复用
public class ObjectPool {
    private static final ThreadLocal<ByteBuffer> bufferPool = 
        ThreadLocal.withInitial(() -> ByteBuffer.allocate(8192));
    
    public void process() {
        ByteBuffer buffer = bufferPool.get();
        buffer.clear();
        // 使用buffer
    }
}

// 4. 关闭GC overhead限制(不推荐,临时方案)
-XX:-UseGCOverheadLimit

🧠 3. Metaspace (Java 8+)

错误信息:

复制代码
java.lang.OutOfMemoryError: Metaspace

核心原因:

加载的类太多,元空间不足

典型场景:

java 复制代码
// 场景1:动态代理大量生成类
public class DynamicProxyOOM {
    public void createManyProxies() {
        for (int i = 0; i < 1000000; i++) {
            MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
                getClass().getClassLoader(),
                new Class[]{MyInterface.class},
                new MyInvocationHandler()
            );
            // 每个代理都会生成新类
        }
    }
}

// 场景2:热部署频繁
// 应用频繁重启,旧类未卸载

// 场景3:大量使用反射
public class ReflectionOOM {
    public void loadManyClasses() throws Exception {
        for (int i = 0; i < 10000; i++) {
            Class<?> clazz = Class.forName("com.example.Class" + i);
            // 每个类都加载到Metaspace
        }
    }
}

排查方法:

bash 复制代码
# 1. 查看元空间使用情况
jstat -gc <pid> | grep MC
# MC: 元空间容量
# MU: 元空间已使用

# 2. 查看加载的类
jcmd <pid> GC.class_stats

# 3. dump类加载信息
-XX:+TraceClassLoading -XX:+TraceClassUnloading

解决方案:

java 复制代码
// 1. 调整Metaspace参数
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
-XX:CompressedClassSpaceSize=256m

// 2. 使用不同的ClassLoader
public class CustomClassLoader extends URLClassLoader {
    public CustomClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }
    
    // 需要时创建新的ClassLoader实例
    // 不需要时,整个ClassLoader可以被回收
}

// 3. 限制动态代理
public class LimitedProxyCreator {
    private static final Map<String, Object> PROXY_CACHE = new ConcurrentHashMap<>();
    
    public MyInterface getProxy(String key) {
        return (MyInterface) PROXY_CACHE.computeIfAbsent(key, k -> 
            Proxy.newProxyInstance(
                getClass().getClassLoader(),
                new Class[]{MyInterface.class},
                new MyInvocationHandler()
            )
        );
    }
}

💾 4. Direct buffer memory

错误信息:

复制代码
java.lang.OutOfMemoryError: Direct buffer memory

核心原因:

堆外内存(Direct Buffer)耗尽

典型场景:

java 复制代码
// 场景1:Netty使用不当
public class NettyOOM {
    public void startServer() {
        // Netty默认使用堆外内存
        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup)
         .channel(NioServerSocketChannel.class)
         .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
         .handler(new LoggingHandler(LogLevel.INFO))
         .childHandler(new ChannelInitializer<SocketChannel>() {
             @Override
             public void initChannel(SocketChannel ch) {
                 // 如果不释放ByteBuf,会导致堆外内存泄露
                 ch.pipeline().addLast(new MyHandler());
             }
         });
    }
}

// 场景2:大量使用ByteBuffer.allocateDirect
public class DirectBufferOOM {
    public void allocateBuffers() {
        List<ByteBuffer> buffers = new ArrayList<>();
        while (true) {
            // 每个Buffer 1MB,但不被GC管理
            buffers.add(ByteBuffer.allocateDirect(1024 * 1024));
        }
    }
}

排查方法:

bash 复制代码
# 1. 查看直接内存使用
jcmd <pid> VM.native_memory summary scale=MB

# 2. 使用NMT(Native Memory Tracking)
-XX:NativeMemoryTracking=summary
jcmd <pid> VM.native_memory detail

# 3. 查看BufferPool
jcmd <pid> ManagementAgent.jmx_invoke sun.nio.ch.BufTracker getDirectBufferPoolCount

解决方案:

java 复制代码
// 1. 限制直接内存大小
-XX:MaxDirectMemorySize=256m

// 2. Netty内存泄露检测
// 添加内存泄露检测
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator())

// 启用泄露检测
ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID);

// 3. 正确释放资源
public class SafeDirectBuffer {
    public void useDirectBuffer() {
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
        try {
            // 使用buffer
            buffer.put("data".getBytes());
        } finally {
            // 重要:手动清理
            if (buffer.isDirect()) {
                ((DirectBuffer) buffer).cleaner().clean();
            }
        }
    }
}

// 4. 使用池化ByteBuf
public class PooledBufferExample {
    private final ByteBufPool bufferPool = new ByteBufPool();
    
    public void process() {
        ByteBuf buf = bufferPool.borrowBuffer();
        try {
            // 使用buf
        } finally {
            bufferPool.returnBuffer(buf);
        }
    }
}

🧵 5. Unable to create new native thread

错误信息:

复制代码
java.lang.OutOfMemoryError: unable to create new native thread

核心原因:

创建的线程数超过系统限制

典型场景:

java 复制代码
// 场景1:递归创建线程
public class RecursiveThreadOOM {
    public void createThreads() {
        while (true) {
            new Thread(() -> {
                try {
                    Thread.sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }).start();
        }
    }
}

// 场景2:线程池配置不当
public class ThreadPoolOOM {
    public void misuseExecutor() {
        // 错误:使用无界队列,线程数会一直增长
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            10,  // corePoolSize
            Integer.MAX_VALUE,  // 最大线程数太大
            60L, TimeUnit.SECONDS,
            new SynchronousQueue<>()  // 队列太小
        );
    }
}

排查方法:

bash 复制代码
# 1. 查看系统线程限制
ulimit -u
cat /proc/sys/kernel/threads-max

# 2. 查看Java进程线程数
pstree -p <pid> | wc -l
jstack <pid> | grep "java.lang.Thread.State" | wc -l

# 3. 查看每个线程的栈大小
jinfo -flag ThreadStackSize <pid>

解决方案:

java 复制代码
// 1. 调整系统参数
// Linux系统
echo 10000 > /proc/sys/kernel/threads-max
ulimit -u 10000

// 2. 调整JVM参数
-Xss256k  # 减小线程栈大小
-XX:VMThreadStackSize=256

// 3. 合理使用线程池
public class SafeThreadPool {
    private final ExecutorService executor = new ThreadPoolExecutor(
        10,  // 核心线程
        100,  // 最大线程
        60L, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(1000),  // 有界队列
        new ThreadFactoryBuilder()
            .setNameFormat("worker-%d")
            .build(),
        new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略
    );
    
    // 4. 使用虚拟线程(Java 19+)
    public void useVirtualThreads() {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 100000; i++) {
                executor.submit(() -> {
                    Thread.sleep(Duration.ofSeconds(1));
                    return "Done";
                });
            }
        }
    }
}

📈 6. Requested array size exceeds VM limit

错误信息:

复制代码
java.lang.OutOfMemoryError: Requested array size exceeds VM limit

核心原因:

尝试创建超过JVM限制的数组

最大限制:

  • 32位JVM:约2^31-1 = 2,147,483,647 元素
  • 64位JVM:约2^31-1(受堆大小限制)

典型场景:

java 复制代码
// 场景:创建超大数组
public class HugeArrayOOM {
    public void createHugeArray() {
        // 尝试创建20亿个元素的int数组
        // 20亿 * 4字节 ≈ 8GB
        int[] hugeArray = new int[2_000_000_000];
    }
}

解决方案:

java 复制代码
// 1. 分批处理
public class BatchArrayProcessor {
    public void processLargeData(long totalSize) {
        int batchSize = 1000000;  // 每批100万
        int batches = (int) Math.ceil((double) totalSize / batchSize);
        
        for (int i = 0; i < batches; i++) {
            int currentSize = Math.min(batchSize, (int)(totalSize - i * batchSize));
            int[] batch = new int[currentSize];
            processBatch(batch);
        }
    }
}

// 2. 使用稀疏数组
public class SparseArrayExample {
    private Map<Integer, Integer> sparseArray = new HashMap<>();
    
    public void set(int index, int value) {
        if (value != 0) {  // 只存储非零值
            sparseArray.put(index, value);
        }
    }
    
    public int get(int index) {
        return sparseArray.getOrDefault(index, 0);
    }
}

// 3. 使用内存映射文件
public class MappedFileArray {
    public void processLargeFile(String filePath, long arraySize) throws IOException {
        try (RandomAccessFile file = new RandomAccessFile(filePath, "rw");
             FileChannel channel = file.getChannel()) {
            
            MappedByteBuffer buffer = channel.map(
                FileChannel.MapMode.READ_WRITE, 0, arraySize * 4
            );
            
            IntBuffer intBuffer = buffer.asIntBuffer();
            // 像操作数组一样操作intBuffer
            for (int i = 0; i < arraySize; i++) {
                intBuffer.put(i, i * 2);
            }
        }
    }
}

🔧 7. Kill process or sacrifice child

错误信息(Linux OOM Killer):

复制代码
Out of memory: Kill process [pid] (java) score [score] or sacrifice child

核心原因:

系统物理内存耗尽,Linux OOM Killer终止进程

排查方法:

bash 复制代码
# 查看OOM Killer日志
dmesg | grep -i "out of memory"
dmesg | grep -i "killed process"

# 查看系统内存
free -h
cat /proc/meminfo

# 查看进程内存
ps aux --sort=-%mem | head -20

解决方案:

java 复制代码
// 1. 调整JVM内存参数
// 不要设置过大,预留系统内存
-Xmx8g  # 8GB堆内存
-Xms8g
-XX:MaxMetaspaceSize=512m
-XX:MaxDirectMemorySize=256m
-XX:ReservedCodeCacheSize=256m

// 2. 使用容器时设置内存限制
# Docker示例
docker run -m 10g --memory-reservation=8g your-java-app

# Kubernetes示例
resources:
  limits:
    memory: "10Gi"
  requests:
    memory: "8Gi"

// 3. 使用Native Memory Tracking监控
-XX:NativeMemoryTracking=detail
jcmd <pid> VM.native_memory baseline
jcmd <pid> VM.native_memory detail.diff

// 4. 调整系统OOM Killer参数
echo 100 > /proc/sys/vm/overcommit_memory
echo 1 > /proc/sys/vm/overcommit_ratio

🎯 实战排查流程

当发生OOM时,按此流程排查:

bash 复制代码
# 第一步:立即保存现场
# 1. 保存错误日志
# 2. 生成堆转储
jmap -dump:live,format=b,file=heap.hprof <pid>

# 第二步:分析内存使用
# 1. 查看堆内存分布
jmap -histo:live <pid> | head -20

# 2. 查看GC情况
jstat -gcutil <pid> 1000 10

# 3. 查看线程状态
jstack <pid> > thread.dump

# 第三步:使用分析工具
# 1. Eclipse MAT
# 2. VisualVM
# 3. JProfiler
# 4. YourKit

# 第四步:复现和监控
# 1. 设置监控参数
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./java_pid<pid>.hprof
-XX:OnOutOfMemoryError="kill -3 %p"
-Xlog:gc*,gc+heap=debug:file=gc_%t.log

📊 预防策略

1. 代码层面

java 复制代码
// 使用内存敏感的数据结构
// 使用WeakHashMap、SoftReference
// 及时关闭资源
// 使用try-with-resources

2. JVM参数优化

bash 复制代码
# 生产环境推荐配置
-Xms4g -Xmx4g
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
-XX:MaxDirectMemorySize=256m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=35
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dumps
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
-Xlog:gc*,gc+heap=debug:file=gc.log

3. 监控告警

yaml 复制代码
# 需要监控的指标:
# 1. 堆内存使用率 > 80% 告警
# 2. GC时间占比 > 20% 告警
# 3. 老年代增长速率
# 4. Metaspace使用率
# 5. 线程数增长
# 6. 直接内存使用

💎 总结

OOM类型 关键特征 优先排查点
heap space 对象太多,GC无法回收 大对象、内存泄露、缓存
GC overhead GC时间长,回收效率低 循环创建对象、字符串处理
Metaspace 类加载过多 动态代理、反射、热部署
Direct buffer 堆外内存不足 Netty、NIO、MMAP
unable to create thread 线程数超标 线程池配置、递归调用
array size 数组过大 大数组创建
OOM Killer 系统内存耗尽 容器限制、物理内存

anyway,预防优于治疗。在生产环境部署前,必须进行压力测试和内存分析,建立完善的监控体系,才能避免OOM对业务造成严重影响。

相关推荐
WangJunXiang62 小时前
GFS分布式文件系统
开发语言·php
民乐团扒谱机2 小时前
【微实验】基于matlab的音频提取与信号滤波处理
开发语言·matlab·音视频
eLIN TECE2 小时前
springboot和springframework版本依赖关系
java·spring boot·后端
SomeB1oody2 小时前
【Python深度学习】3.4. 循环神经网络(RNN)实战:预测股价
开发语言·人工智能·python·rnn·深度学习·机器学习
良木生香2 小时前
【C++初阶】:STL——String从入门到应用完全指南(1)
c语言·开发语言·数据结构·c++·算法
老神在在0012 小时前
Spring Bean 的六种作用域详解
java·后端·spring
仙草不加料2 小时前
互联网大厂Java面试故事实录:三轮场景化技术提问与详细答案解析
java·spring boot·微服务·面试·aigc·电商·内容社区
程序员老邢2 小时前
【技术底稿 19】Redis7 集群密码配置 + 权限锁死 + 磁盘占满连锁故障真实排查全记录
java·服务器·经验分享·redis·程序人生·微服务
Bug 挖掘机2 小时前
一篇理清Prompt,Skill,MCP之间的区别
开发语言·软件测试·python·功能测试·测试开发·prompt·ai测试