惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The GitHub Blog
The GitHub Blog
Jina AI
Jina AI
月光博客
月光博客
博客园 - Franky
小众软件
小众软件
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
Visual Studio Blog
有赞技术团队
有赞技术团队
V
V2EX
IT之家
IT之家
阮一峰的网络日志
阮一峰的网络日志
Stack Overflow Blog
Stack Overflow Blog
H
Help Net Security
Apple Machine Learning Research
Apple Machine Learning Research
腾讯CDC
D
DataBreaches.Net
Hugging Face - Blog
Hugging Face - Blog
Martin Fowler
Martin Fowler
罗磊的独立博客
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
WordPress大学
WordPress大学
C
Check Point Blog
Microsoft Azure Blog
Microsoft Azure Blog
Microsoft Security Blog
Microsoft Security Blog

博客园 - buguge

交易成功还要记 3 笔账?我们直接砍到 1 笔—— 网络红包记账的一次自顶向下业务层重构 分享一次针对hardcode的小重构:不要把 package 名写死在代码里 聚合系统设计:如何为银行代付通道抽象出公共的代付接口能力 如何自定义 MyBatis枚举处理器(EnumTypeHandler):分享从 “同包同名 shadow 覆盖” 到 “官方扩展点” 的重构经历 这儿也改状态,那儿也改状态,连AI都理不清楚了… 不妨试试这个状态机good-practice 业务解耦的经典实践:从订单表解耦开票业务说起(以滴滴网约车开票为例) 从 `int` 到 `Duration`:一个缓存 API 的三次演进教会我的事 最差实践(bad-practice):开发者在方法里直接实例化线程池对象,然后...(应用gg了) 【HttpClient最差实践(bad-practice)】开发者在 http 工具方法中直接实例化 HttpClient,然后…… Crypto、Cipher与Password:Java加密开发的三个核心概念 知识VS技能:如何优雅判空? 20260604SR超时问题排查 推敲见文章:从 `try..catch` 看异常日志打印的正确姿势 #解决问题要彻底# 慢SQL治理完成后,如何防止同类问题“死灰复燃”? 从合同甲方是荒谬的“JD”谈起:软件开发不应遗忘的“常识” 别留小尾巴/尽快剪掉小尾巴:从一次“ABA”字段重命名,谈谈“解决问题要彻底” 开发者暴露了一个无需授权访问的裸接口,我问:如果有人暴力请求怎么办? 【SQL性能优化篇】有了!治理慢SQL“WHERE create_time ORDER BY id”的良药---规避“Using filesort”性能杀手 高效查询商户日终余额:一个SQL的优化实践 Hutool 的 `TimedCache` 到期会自动清理吗? ——————hutool cache的"惰性清理"和"定期清理" Fastjson枚举反序列化:当字符串不是枚举常量名时,会发生什么? fastjson-EnumDeserializer类及源码分析 随笔20260309:我们都是围城里的人 `UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only` 异常解析 认识2个单词:goal/target —————— 为什么Maven是 "goal" 而不是 "target"? 聚合系统设计:策略模式(Strategy Pattern)在银行通道对接场景中的应用 这样构建对象,太帅了!—— 阶梯式Builder模式与代码整洁之道 注意!字段数据类型不匹配,这个sql会很慢 还在用ArrayList?用HashSet吧!--性能对比 分页查询还在用create_time去做降序?
常见的OOM错误 ( OutOfMemoryError全类型详解)
buguge · 2026-04-21 · via 博客园 - buguge

📊 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回收

典型场景:

// 场景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());
        }
    }
}

排查步骤:

# 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

解决方案:

// 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都满足上述条件

典型场景:

// 场景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很快失去引用,成为垃圾
        }
    }
}

排查方法:

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

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

解决方案:

// 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

错误信息:

java.lang.OutOfMemoryError: Metaspace

核心原因:

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

典型场景:

// 场景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
        }
    }
}

排查方法:

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

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

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

解决方案:

// 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)耗尽

典型场景:

// 场景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));
        }
    }
}

排查方法:

# 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

解决方案:

// 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

核心原因:

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

典型场景:

// 场景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<>()  // 队列太小
        );
    }
}

排查方法:

# 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>

解决方案:

// 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(受堆大小限制)

典型场景:

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

解决方案:

// 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终止进程

排查方法:

# 查看OOM Killer日志
dmesg | grep -i "out of memory"
dmesg | grep -i "killed process"

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

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

解决方案:

// 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时,按此流程排查:

# 第一步:立即保存现场
# 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. 代码层面

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

2. JVM参数优化

# 生产环境推荐配置
-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. 监控告警

# 需要监控的指标:
# 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对业务造成严重影响。