JVM 内存溢出(OOM)预防策略

Abstract

线程池使用不当是 OOM 的常见诱因之一。理解 OOM 的类型预防手段,是从”会用线程池”到”用好线程池”的关键一步。

OOM 常见类型

类型原因典型错误信息
堆溢出对象无法回收,内存耗尽OutOfMemoryError: Java heap space
栈溢出递归过深或线程过多OutOfMemoryError: unable to create new native thread
元空间溢出类加载过多OutOfMemoryError: Metaspace

栈溢出与线程池的关系

unable to create new native thread 是线程池场景中容易忽略的 OOM。

每个平台线程约占用 ~1MB 栈空间(-Xss 参数控制)。当线程池的 maximumPoolSize 设置过大,或使用 Executors.newCachedThreadPool()(最大容量为 Integer.MAX_VALUE)时,可能创建大量线程导致操作系统无法再创建新的线程

线程池相关的 OOM 预防

// ✅ 安全的线程池配置
public class SafeThreadPoolConfig {
    public static ExecutorService createSafeThreadPool() {
        int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
        int maxPoolSize = corePoolSize * 2;
 
        return new ThreadPoolExecutor(
            corePoolSize,
            maxPoolSize,
            60L,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(1000),   // 有界队列(关键!)
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.CallerRunsPolicy()  // 降级策略
        );
    }
}

三条核心防线

任务提交 → [第一道] 有界队列 → [第二道] 最大线程限制 → [第三道] 拒绝策略
             防止队列无限增长      防止无限创建线程        降级/抛异常
  1. 有界队列 — 队列有容量上限,不会无限增长
  2. 合理的线程上限 — 避免创建过多线程压垮系统
  3. 拒绝策略线程池拒绝策略 是最后的保护手段

数据库查询的内存优化

大量数据一次性加载到内存是 OOM 的另一大元凶。

方式一:JDBC 流式查询

public class StreamQueryExample {
    // 使用 JDBC 流式查询(逐行读取,不缓存全部结果)
    public void processOrdersStream(Long storeId) throws SQLException {
        String sql = "SELECT id, amount FROM orders WHERE store_id = ?";
 
        try (Connection conn = getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql,
                 ResultSet.TYPE_FORWARD_ONLY,
                 ResultSet.CONCUR_READ_ONLY)) {
 
            stmt.setLong(1, storeId);
            stmt.setFetchSize(Integer.MIN_VALUE);  // ⚡ 启用流式获取
 
            try (ResultSet rs = stmt.executeQuery()) {
                long total = 0;
                while (rs.next()) {
                    total += rs.getLong("amount");
                    // 每行处理,不会将所有结果加载到内存
                }
            }
        }
    }
}

setFetchSize(Integer.MIN_VALUE) 的含义

在 MySQL JDBC 驱动中,设置 fetch size 为 Integer.MIN_VALUE逐行读取结果集,而非一次性将全部结果加载到内存。这是防止大查询 OOM 的经典手段

方式二:分页查询

public long calculateSalesWithPagination(Long storeId) {
    long total = 0;
    int pageNum = 0;
    int pageSize = 1000;
 
    while (true) {
        Page<Order> page = orderRepository.findByStoreId(
            storeId,
            PageRequest.of(pageNum, pageSize)
        );
 
        if (page.isEmpty()) break;
 
        total += page.getContent().stream()
            .mapToLong(Order::getAmount)
            .sum();
 
        pageNum++;
    }
    return total;
}

流式 vs 分页选型

  • 流式查询:适合全表扫描、数据量极大(百万级以上),内存占用最低
  • 分页查询:适合需要聚合计算(如 COUNT、SUM)的场景,每页控制内存

JVM 参数配置建议

# 生产环境推荐配置
java -Xms4g \                      # 初始堆大小
     -Xmx8g \                      # 最大堆大小(根据服务器配置调整)
     -XX:MetaspaceSize=256m \
     -XX:MaxMetaspaceSize=512m \
     -XX:+UseG1GC \                # 使用 G1 垃圾收集器
     -XX:MaxGCPauseMillis=200 \    # 最大GC停顿时间(毫秒)
     -XX:+HeapDumpOnOutOfMemoryError \   # OOM时自动生成堆转储
     -XX:HeapDumpPath=/tmp/heapdump.hprof \
     -jar application.jar

参数说明

参数作用
-Xms / -Xmx堆初始/最大大小(建议设为相同值,避免动态调整)
-XX:+UseG1GCG1 收集器,适合大堆内存,暂停时间可控
-XX:+HeapDumpOnOutOfMemoryErrorOOM 时自动 dump,事后分析根因
-Xss线程栈大小(默认 ~1MB,减少可缓解线程数上限)

防 OOM 完整 Checklist

  • 使用有界队列(设置合理容量)
  • 避免使用 Executors.newCachedThreadPool()
  • 设置合理的 maximumPoolSize,防止创建过多线程
  • 数据库查询使用分页 / 流式读取
  • 及时释放不再使用的对象引用
  • 设置合理的 JVM 堆内存大小
  • 配置 OOM 自动堆转储
  • 监控队列积压(见 线程池监控与调优

相关笔记