)
OOM 常见类型总览错误类型触发区域根本原因常见场景Java heap space堆内存对象太多堆放不下内存泄露、大对象、流量突增GC overhead limit exceeded堆内存GC效率低下内存泄露、堆太小、代码问题Metaspace元空间类加载太多动态代理、反射、热部署Direct buffer memory直接内存堆外内存不足NIO、Netty、MMAPUnable 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 Listbyte[] 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 MapString, String cache new HashMap(); public void loadDataToCache() { // 从数据库加载大量数据到内存 ListUser users userDao.findAll(); // 百万条记录 for (User user : users) { cache.put(user.getId(), user.serialize()); } } }排查步骤# 1. 查看当前堆内存使用 jmap -heap pid # 2. 生成堆转储文件 jmap -dump:formatb,fileheap.hprof pid # 3. 实时监控GC jstat -gc pid 1000 # 每秒打印一次 # 4. 使用jcmd jcmd pid GC.heap_info解决方案// 1. 合理设置JVM参数 // 生产环境示例 -Xms4g -Xmx4g // 堆内存4G避免动态扩展 -XX:UseG1GC // 使用G1垃圾收集器 -XX:MaxGCPauseMillis200 // 目标暂停时间 -XX:HeapDumpOnOutOfMemoryError // OOM时自动dump -XX:HeapDumpPath/path/to/dumps // 2. 修复内存泄露代码 public class FixedMemoryLeak { // 使用弱引用或软引用 private static final MapString, SoftReferenceBigObject CACHE new WeakHashMap(); // 或使用LRU缓存 private static final MapString, BigObject safeCache Collections.synchronizedMap(new LinkedHashMapString, 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) { ListData 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*,gcheapdebug:filegc.log -Xmx512m YourApp # 2. 使用VisualGC或JConsole监控 # 3. 分析GC日志文件解决方案// 1. 优化JVM参数 -Xmx2g -Xms2g -XX:UseG1GC -XX:MaxGCPauseMillis200 -XX:G1ReservePercent15 -XX:InitiatingHeapOccupancyPercent35 // 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 ThreadLocalByteBuffer 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核心原因加载的类太多元空间不足典型场景// 场景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:MetaspaceSize256m -XX:MaxMetaspaceSize512m -XX:UseCompressedClassPointers -XX:UseCompressedOops -XX:CompressedClassSpaceSize256m // 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 MapString, 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耗尽典型场景// 场景1Netty使用不当 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 ChannelInitializerSocketChannel() { Override public void initChannel(SocketChannel ch) { // 如果不释放ByteBuf会导致堆外内存泄露 ch.pipeline().addLast(new MyHandler()); } }); } } // 场景2大量使用ByteBuffer.allocateDirect public class DirectBufferOOM { public void allocateBuffers() { ListByteBuffer buffers new ArrayList(); while (true) { // 每个Buffer 1MB但不被GC管理 buffers.add(ByteBuffer.allocateDirect(1024 * 1024)); } } }排查方法# 1. 查看直接内存使用 jcmd pid VM.native_memory summary scaleMB # 2. 使用NMTNative Memory Tracking -XX:NativeMemoryTrackingsummary jcmd pid VM.native_memory detail # 3. 查看BufferPool jcmd pid ManagementAgent.jmx_invoke sun.nio.ch.BufTracker getDirectBufferPoolCount解决方案// 1. 限制直接内存大小 -XX:MaxDirectMemorySize256m // 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:VMThreadStackSize256 // 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 MapInteger, 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 KillerOut 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:MaxMetaspaceSize512m -XX:MaxDirectMemorySize256m -XX:ReservedCodeCacheSize256m // 2. 使用容器时设置内存限制 # Docker示例 docker run -m 10g --memory-reservation8g your-java-app # Kubernetes示例 resources: limits: memory: 10Gi requests: memory: 8Gi // 3. 使用Native Memory Tracking监控 -XX:NativeMemoryTrackingdetail 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,formatb,fileheap.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_pidpid.hprof -XX:OnOutOfMemoryErrorkill -3 %p -Xlog:gc*,gcheapdebug:filegc_%t.log回到顶部 预防策略1. 代码层面// 使用内存敏感的数据结构 // 使用WeakHashMap、SoftReference // 及时关闭资源 // 使用try-with-resources2. JVM参数优化# 生产环境推荐配置 -Xms4g -Xmx4g -XX:MetaspaceSize256m -XX:MaxMetaspaceSize512m -XX:MaxDirectMemorySize256m -XX:UseG1GC -XX:MaxGCPauseMillis200 -XX:InitiatingHeapOccupancyPercent35 -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/path/to/dumps -XX:UseCompressedClassPointers -XX:UseCompressedOops -Xlog:gc*,gcheapdebug:filegc.log3. 监控告警# 需要监控的指标 # 1. 堆内存使用率 80% 告警 # 2. GC时间占比 20% 告警 # 3. 老年代增长速率 # 4. Metaspace使用率 # 5. 线程数增长 # 6. 直接内存使用回到顶部 总结OOM类型关键特征优先排查点heap space对象太多GC无法回收大对象、内存泄露、缓存GC overheadGC时间长回收效率低循环创建对象、字符串处理Metaspace类加载过多动态代理、反射、热部署Direct buffer堆外内存不足Netty、NIO、MMAPunable to create thread线程数超标线程池配置、递归调用array size数组过大大数组创建OOM Killer系统内存耗尽容器限制、物理内存