Java

发布于

AI总结: 本文介绍了OOM(Out Of Memory)常见类型及其原因、场景、排查方法和解决方案。OOM的类型包括Java heap space、GC overhead limit exceeded、Metaspace、Direct buffer memory、Unable to create new native thread、Requested array size exceeds VM limit和Kill process or sacrifice child。每种类型都有其核心原因和典型场景,并提供了相应的排查和解决方案。此外,文中还介绍了OOM排查工具和实战流程,以及预防策略,包括代码层面优化、JVM参数设置、监控告警和压力测试。 改进建议:可以增加对每种OOM类型的具体案例分析,提供更详细的代码示例和性能优化建议,以帮助开发者更好地理解和应对OOM问题。

OOM 常见类型

OOM (Out Of Memory) 是指应用程序在运行时内存不足,无法分配新的内存空间,导致程序崩溃或异常终止的现象。

错误类型 触发区域 根本原因 常见场景
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回收

  • 堆内存设置过小
  • 内存泄漏(对象无法被 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% 的内存
  • 通常是内存泄漏的前兆

典型场景:

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

🧠 3. Metaspace (Java 8+)

错误信息:

java.lang.OutOfMemoryError: Metaspace  

核心原因:

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

  • 类加载过多(动态代理、反射)
  • 类加载器泄漏
  • 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)耗尽

  • NIO 直接内存使用过多
  • 未正确释放 DirectByteBuffer

典型场景:

// 场景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. JVM 自带工具

jps - 查看 Java 进程

# 查看所有 Java 进程  
jps -l  

# 查看进程及主类  
jps -lv  

jstat - 查看 GC 统计信息

# 查看 GC 情况,每 1000ms 输出一次  
jstat -gc <pid> 1000  

# 查看堆内存使用情况  
jstat -gccapacity <pid>  

# 查看 GC 原因  
jstat -gccause <pid> 1000  

jmap - 生成堆转储文件

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

# 查看堆内存使用情况  
jmap -heap <pid>  

# 查看堆中对象统计信息  
jmap -histo <pid> | head -n 20  

jstack - 查看线程堆栈

# 查看线程堆栈  
jstack <pid> > thread.txt  

# 查看死锁  
jstack -l <pid>  

2. 可视化分析工具

MAT (Memory Analyzer Tool)

  • 分析堆转储文件
  • 查找内存泄漏
  • 对象引用链分析

JProfiler

  • 实时监控内存使用
  • CPU 性能分析
  • 线程分析

VisualVM

  • JDK 自带的可视化工具
  • 实时监控
  • 堆转储分析

Arthas

  • 阿里开源的 Java 诊断工具
  • 无需重启应用
  • 实时查看内存、线程、GC 等信息
# 启动 Arthas  
java -jar arthas-boot.jar  

# 查看 JVM 信息  
dashboard  

# 查看堆内存  
memory  

# 生成堆转储  
heapdump /tmp/heap.hprof  

🎯 实战排查流程

当发生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  

✅ 使用对象池

// 使用 Apache Commons Pool  
GenericObjectPool<ExpensiveObject> pool = new GenericObjectPool<>(  
    new ExpensiveObjectFactory(),  
    new GenericObjectPoolConfig()  
);  

// 使用对象  
ExpensiveObject obj = pool.borrowObject();  
try {  
    // 使用对象  
} finally {  
    pool.returnObject(obj); // 归还对象  
}  

✅ 及时释放资源

// 使用 try-with-resources  
try (Connection conn = dataSource.getConnection();  
     PreparedStatement ps = conn.prepareStatement(sql);  
     ResultSet rs = ps.executeQuery()) {  
    // 处理结果  
}  

✅ 避免创建不必要的对象

// ❌ 错误:每次都创建新对象  
public String formatDate(Date date) {  
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");  
    return sdf.format(date);  
}  

// ✅ 正确:使用 ThreadLocal 复用对象  
private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT =  
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));  

public String formatDate(Date date) {  
    return DATE_FORMAT.get().format(date);  
}  

// ✅ 更好:使用 Java 8 的 DateTimeFormatter(线程安全)  
private static final DateTimeFormatter FORMATTER =  
    DateTimeFormatter.ofPattern("yyyy-MM-dd");  

public String formatDate(LocalDate date) {  
    return date.format(FORMATTER);  
}  

2. JVM参数优化

# 基础内存设置  
-Xms4g                          # 初始堆大小  
-Xmx4g                          # 最大堆大小(建议与 Xms 相同,避免动态扩容)  
-Xmn2g                          # 年轻代大小  
-Xss256k                        # 每个线程的栈大小  

# Metaspace 设置  
-XX:MetaspaceSize=256m          # Metaspace 初始大小  
-XX:MaxMetaspaceSize=512m       # Metaspace 最大大小  

# GC 设置(G1 GC)  
-XX:+UseG1GC                    # 使用 G1 垃圾收集器  
-XX:MaxGCPauseMillis=200        # 最大 GC 停顿时间  
-XX:G1HeapRegionSize=16m        # G1 Region 大小  

# OOM 时自动生成堆转储  
-XX:+HeapDumpOnOutOfMemoryError  
-XX:HeapDumpPath=/var/logs/heapdump/  

# GC 日志  
-Xlog:gc*:file=/var/logs/gc.log:time,uptime,level,tags  
-XX:+PrintGCDetails  
-XX:+PrintGCDateStamps  

# 其他优化  
-XX:+DisableExplicitGC          # 禁用 System.gc()  
-XX:+UseStringDeduplication     # 字符串去重(G1 GC)  

# 生产环境推荐配置  
-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. 直接内存使用  

Prometheus + Grafana 监控

# prometheus.yml  
scrape_configs:  
  - job_name: 'java-app'  
    metrics_path: '/actuator/prometheus'  
    static_configs:  
      - targets: ['localhost:8080']  

关键指标监控

  • 堆内存使用率: jvm_memory_used_bytes / jvm_memory_max_bytes
  • GC 频率: rate(jvm_gc_pause_seconds_count[5m])
  • GC 耗时: jvm_gc_pause_seconds_sum
  • 线程数: jvm_threads_live_threads

告警规则

groups:  
  - name: jvm_alerts  
    rules:  
      # 堆内存使用率超过 85%  
      - alert: HighHeapMemoryUsage  
        expr: (jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"}) > 0.85  
        for: 5m  
        annotations:  
          summary: "堆内存使用率过高"  

      # GC 耗时超过 1 秒  
      - alert: HighGCTime  
        expr: rate(jvm_gc_pause_seconds_sum[5m]) > 1  
        for: 5m  
        annotations:  
          summary: "GC 耗时过长"  

4. 压力测试

使用 JMeter、Gatling 等工具进行压力测试:

# JMeter 压力测试  
jmeter -n -t test-plan.jmx -l results.jtl  

# 同时监控内存使用  
jstat -gc <pid> 1000 > gc-stats.log  

🎃 总结

OOM 排查核心要点

  1. 及时发现: 配置监控告警,第一时间发现问题
  2. 保留现场: 配置 -XX:+HeapDumpOnOutOfMemoryError
  3. 分析定位: 使用 MAT 等工具分析堆转储文件
  4. 修复验证: 修改代码后进行压力测试验证
  5. 预防为主: 代码规范、资源管理、容量规划

最佳实践清单

  • [ ] JVM 参数合理配置
  • [ ] 开启 OOM 时自动生成堆转储
  • [ ] 开启 GC 日志
  • [ ] 配置内存监控告警
  • [ ] 定期进行压力测试
  • [ ] 代码审查关注内存使用
  • [ ] 使用对象池复用对象
  • [ ] 及时释放资源(try-with-resources)
  • [ ] 避免大对象一次性加载
  • [ ] 合理使用缓存(设置过期时间和容量限制)