首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Java 开发者必看:工作中最容易踩坑的 OOM 问题全解析(附解决方案与实战案例)

Java 开发者必看:工作中最容易踩坑的 OOM 问题全解析(附解决方案与实战案例)

作者头像
果酱带你啃java
发布2026-04-14 11:05:28
发布2026-04-14 11:05:28
590
举报

在 Java 开发的职业生涯中,OutOfMemoryError(简称 OOM)就像一颗随时可能引爆的炸弹,常常在生产环境中突然爆发,给系统稳定性带来致命打击。OOM 问题不仅难以排查,更因其出现的随机性和破坏性让许多开发者头疼不已。

据统计,生产环境中约 30% 的严重故障与内存问题相关,而 OOM 更是其中的头号杀手。无论是刚入行的新手还是资深工程师,都可能在不经意间写出导致 OOM 的代码。更棘手的是,很多 OOM 问题在测试环境难以复现,却在流量高峰期突然发难,让排查工作雪上加霜。

本文将系统梳理工作中最常见的几种 OOM 问题,从底层原理到代码实践,从排查方法到解决方案,全方位剖析 OOM 的前世今生。每个案例都基于真实生产场景改编,包含可复现的代码示例和符合阿里巴巴开发规范的最佳实践,帮你彻底掌握 OOM 问题的应对之道。

一、Java 堆内存溢出(java.lang.OutOfMemoryError: Java heap space)

Java 堆(Heap)是 OOM 问题的重灾区,几乎每个 Java 开发者都或多或少遇到过堆内存溢出的情况。堆内存用于存储对象实例,当我们创建的对象无法被垃圾回收,且总大小超过堆内存限制时,就会抛出Java heap space错误。

1.1 堆内存溢出的典型场景

堆内存溢出通常有两种常见原因:

  • 内存泄漏(Memory Leak):对象不再被使用但仍然被引用,导致无法回收,逐渐耗尽堆内存
  • 内存溢出(Memory Overflow):对象确实还在被使用,但创建的对象数量或大小超过了堆内存限制
1.1.1 内存泄漏导致的 OOM

内存泄漏是最隐蔽的 OOM 原因,常见于长生命周期对象持有短生命周期对象的引用。例如:静态集合类缓存对象后未及时清理、监听器未正确移除、连接未关闭等。

实战案例:未清理的缓存导致内存泄漏运行

代码语言:javascript
复制
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
 * 演示内存泄漏导致的堆内存溢出
 * 场景:静态集合缓存大量临时对象且未清理
 */
public class HeapOOMLeakDemo {
    private static final Logger logger = LoggerFactory.getLogger(HeapOOMLeakDemo.class);

    // 静态集合,生命周期与应用一致
    private static final List<User> userCache = new ArrayList<>();
    static class User {
        // 模拟用户数据,占用一定内存
        private byte[] data = new byte[1024 * 1024]; // 1MB
        private String username;
        public User(String username) {
            this.username = username;
        }
    }
    public static void main(String[] args) {
        try {
            int count = 0;
            while (true) {
                // 不断创建用户对象并加入静态缓存
                User user = new User("user-" + count);
                userCache.add(user);
                count++;

                if (count % 100 == 0) {
                    logger.info("已缓存 {} 个用户对象", count);
                    TimeUnit.MILLISECONDS.sleep(100);
                }
            }
        } catch (InterruptedException e) {
            logger.error("线程中断", e);
            Thread.currentThread().interrupt();
        } catch (OutOfMemoryError e) {
            logger.error("发生堆内存溢出", e);
            // 实际生产中可在这里添加报警、内存dump等处理
        }
    }
}

运行配置(限制堆大小以快速复现):

代码语言:javascript
复制
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof
代码语言:javascript
复制


现象分析:程序会不断创建 User 对象并加入静态集合,由于静态集合的生命周期与应用一致,这些 User 对象永远不会被回收,最终导致堆内存溢出。日志会输出类似:

INFO HeapOOMLeakDemo - 已缓存 100 个用户对象 INFO HeapOOMLeakDemo - 已缓存 200 个用户对象 ERROR HeapOOMLeakDemo - 发生堆内存溢出 java.lang.OutOfMemoryError: Java heap space at HeapOOMLeakDemo$User.<init>(HeapOOMLeakDemo.java:23) at HeapOOMLeakDemo.main(HeapOOMLeakDemo.java:34)

1.1.2 内存溢出导致的 OOM

即使没有内存泄漏,当应用需要创建的对象总大小超过堆内存限制时,也会发生 OOM。这种情况常见于处理大量数据(如批量导入、大文件解析)时。

实战案例:处理大量数据时的堆溢出

代码语言:javascript
复制
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
 * 演示正常使用但对象过大导致的堆内存溢出
 * 场景:批量处理大量数据时内存不足
 */
public class HeapOOMOverflowDemo {
    private static final Logger logger = LoggerFactory.getLogger(HeapOOMOverflowDemo.class);
    public static void main(String[] args) {
        try {
            // 模拟处理100万条记录
            int batchSize = 1000000;
            List<String> dataList = new ArrayList<>(batchSize);

            logger.info("开始处理 {} 条数据", batchSize);

            for (int i = 0; i < batchSize; i++) {
                // 生成随机字符串模拟数据
                String data = UUID.randomUUID().toString() + 
                             UUID.randomUUID().toString() + 
                             UUID.randomUUID().toString();
                dataList.add(data);

                if (i % 100000 == 0) {
                    logger.info("已处理 {} 条数据", i);
                }
            }

            logger.info("数据处理完成,共 {} 条", dataList.size());
        } catch (OutOfMemoryError e) {
            logger.error("处理数据时发生堆内存溢出", e);
        }
    }
}

运行配置

-Xms50m -Xmx50m -XX:+HeapDumpOnOutOfMemoryError

现象分析:程序需要处理 100 万条数据,每条数据都包含 3 个 UUID 字符串,总内存占用超过 50MB 的堆限制,导致 OOM。这种情况并非内存泄漏(数据确实在被使用),而是内存不足。

1.2 堆内存溢出的排查方法

排查堆内存溢出需要结合日志分析、内存快照和代码审查,常用步骤如下:

  1. 开启内存快照:在 JVM 参数中添加-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof,让 JVM 在 OOM 时自动生成内存快照。
  2. 分析内存快照:使用 MAT(Memory Analyzer Tool)或 JProfiler 等工具分析快照:
    • 查看支配树(Dominator Tree),找出占用内存最多的对象
    • 分析对象引用链,确定哪些对象持有它们的引用
    • 检查是否有异常的对象数量(如某个类的实例数远超预期)
  3. 结合代码定位问题:根据内存分析结果,在代码中查找对应的对象创建和引用逻辑,判断是内存泄漏还是正常内存不足。
  4. 监控内存使用:在生产环境使用 JVisualVM、Prometheus+Grafana 等工具监控堆内存使用趋势,提前发现潜在问题。

MAT 工具使用示例

  • 打开 heapdump.hprof 文件
  • 选择 "Leak Suspects" 报告,查看可能的内存泄漏点
  • 通过 "Histogram" 查看各类型对象的数量和内存占用
  • 使用 "Merge Shortest Paths to GC Roots" 分析对象引用链

1.3 堆内存溢出的解决方案

针对不同原因的堆内存溢出,解决方案也有所不同:

1.3.1 解决内存泄漏问题
  • 及时清理缓存:对于静态缓存,设置合理的过期策略或使用 WeakHashMap 等自动清理机制
代码语言:javascript
复制
// 改进:使用WeakHashMap自动回收不再被引用的对象
private static final Map<String, User> userCache = new WeakHashMap<>();
// 或使用Guava的Cache设置过期时间
private static final LoadingCache<String, User> userCache = CacheBuilder.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES) // 10分钟未访问自动过期
    .maximumSize(1000) // 最大缓存数量
    .build(new CacheLoader<String, User>() {
        @Override
        public User load(String key) {
            return createUser(key); // 加载用户的逻辑
        }
    });
  • 避免长生命周期对象持有短生命周期对象:例如,将方法内的临时对象存储到静态集合中时要特别小心
  • 释放资源引用:对于监听器、回调等,在不需要时及时移除;对于数据库连接、文件流等资源,使用 try-with-resources 确保关闭
  • 使用弱引用:对于非必需的引用,使用 WeakReference 或 SoftReference,允许 GC 在内存不足时回收
1.3.2 解决内存溢出问题

合理调整堆内存大小:根据应用需求和服务器配置,设置合适的-Xms-Xmx参数(通常两者设置为相同值避免频繁扩容)

plaintext

# 生产环境常见配置(根据实际情况调整) -Xms2g -Xmx2g

分批处理数据:对于大量数据,采用分批处理策略,避免一次性加载全部数据到内存

代码语言:javascript
复制
代码语言:javascript
复制
// 改进:分批处理数据
public void processDataInBatches() {
    int total = 1000000;
    int batchSize = 1000;
    int batches = (total + batchSize - 1) / batchSize;

    logger.info("共需处理 {} 条数据,分 {} 批处理", total, batches);

    for (int i = 0; i < batches; i++) {
        int start = i * batchSize;
        int end = Math.min(start + batchSize, total);
        List<String> batchData = loadBatchData(start, end); // 加载当前批次数据
        processBatch(batchData); // 处理当前批次
        batchData.clear(); // 主动清理,帮助GC
        logger.info("已完成第 {} 批处理,共处理 {} 条", i + 1, end);
    }
}
  • 使用内存高效的数据结构:例如,使用基本类型数组代替包装类集合,使用 StringBuilder 代替 String 拼接等
  • 对象复用:对于频繁创建的对象(如循环中),考虑使用对象池复用对象,减少 GC 压力

1.4 阿里巴巴开发规范中的相关要求

  • 【强制】集合初始化时,指定集合初始值大小。 // 正例 List<User> userList =newArrayList<>(100);// 已知大概需要存储100个元素 // 反例 List<User> userList =newArrayList<>();// 未指定初始大小,可能导致多次扩容
  • 【建议】小心 String.intern () 的使用。常量池的字符串过多也可能导致 OOM。
  • 【强制】避免在循环中创建对象,尤其是大对象。
  • 【建议】使用 try-with-resources 关闭资源,避免资源泄漏间接导致内存泄漏。

二、方法区 / 元空间溢出(java.lang.OutOfMemoryError: Metaspace)

在 JDK 8 及以上版本中,方法区的实现由元空间(Metaspace)替代了永久代(PermGen)。元空间用于存储类信息、常量、静态变量、JIT 编译后的代码等。当元空间不足时,会抛出Metaspace相关的 OOM 错误。

2.1 元空间溢出的典型场景

元空间溢出通常与类加载有关,常见场景包括:

  • 动态生成大量类(如使用 CGLIB、JDK 动态代理等)
  • 频繁部署和卸载应用(如 OSGi 环境或热部署场景)
  • 第三方框架或库使用不当导致类无限增多
  • 元空间大小设置过小

实战案例:动态生成类导致的元空间溢出

代码语言:javascript
复制
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * 演示元空间溢出
 * 场景:使用CGLIB动态生成大量代理类
 */
public class MetaspaceOOMDemo {
    private static final Logger logger = LoggerFactory.getLogger(MetaspaceOOMDemo.class);
    static class TargetClass {
        // 目标类,将被动态代理
    }
    public static void main(String[] args) {
        try {
            int count = 0;
            while (true) {
                // 使用CGLIB动态生成代理类
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(TargetClass.class);
                enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));

                // 生成并加载代理类
                Class<?> proxyClass = enhancer.createClass();
                count++;

                if (count % 100 == 0) {
                    logger.info("已生成 {} 个代理类", count);
                }
            }
        } catch (OutOfMemoryError e) {
            logger.error("发生元空间溢出", e);
        }
    }
}

Maven 依赖

<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>

运行配置(限制元空间大小以快速复现):

-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m -XX:+HeapDumpOnOutOfMemoryError

现象分析:程序使用 CGLIB 不断生成 TargetClass 的代理类,每个代理类都是一个新的 Class 对象,被加载到元空间。由于元空间大小被限制为 10MB,最终会因无法加载新类而抛出 OOM:

INFO MetaspaceOOMDemo - 已生成 100 个代理类 INFO MetaspaceOOMDemo - 已生成 200 个代理类 ... ERROR MetaspaceOOMDemo - 发生元空间溢出 java.lang.OutOfMemoryError: Metaspace at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:345) at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492) at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:114) at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:291) at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480) at net.sf.cglib.proxy.Enhancer.createClass(Enhancer.java:337) at MetaspaceOOMDemo.main(MetaspaceOOMDemo.java:27)

2.2 元空间的工作原理

理解元空间的工作原理有助于我们更好地排查和解决相关 OOM 问题:

  • 元空间与永久代的区别:元空间不在 JVM 堆内存中,而是使用本地内存(Native Memory),理论上可以利用系统的所有可用内存(受限于 OS 和进程内存限制)。
  • 元空间的组成
    • 类的元数据(方法、字段、注解等信息)
    • 常量池(字符串常量池在 JDK 7 后移至堆内存)
    • 类加载器数据
    • JIT 编译后的代码
  • 元空间的动态扩容:元空间会根据需要动态扩容,但受MaxMetaspaceSize限制(默认无上限,可能导致系统内存耗尽)。
  • 元空间的回收:当类加载器被回收时,其加载的类元数据也会被回收。因此,类元数据的回收依赖于类加载器的回收。

2.3 元空间溢出的排查方法

元空间溢出的排查相对复杂,需要结合类加载情况和元空间使用监控:

  1. 监控元空间使用情况
    • 使用jstat -gcmetacapacity <pid>查看元空间容量和使用情况
    • 使用 JVisualVM 的监视面板观察元空间趋势
  2. 分析类加载情况
    • 使用jmap -clstats <pid>查看类加载统计信息
    • 使用jcmd <pid> GC.class_stats获取详细的类统计(需要 JDK 10+)
    • 检查是否有异常的类数量增长
  3. 定位问题类加载器
    • 使用 MAT 分析内存快照中的类加载器实例
    • 检查是否有大量自定义类加载器未被回收
    • 分析类加载器的层次结构和引用关系
  4. 检查动态类生成
    • 排查使用 CGLIB、Javassist、JDK 动态代理等技术的代码
    • 检查是否有无限生成类的逻辑

2.4 元空间溢出的解决方案

针对元空间溢出,主要从合理配置和代码优化两方面解决:

2.4.1 合理配置元空间参数

设置元空间初始大小和最大大小:避免元空间无限制增长,也避免初始大小过小导致频繁扩容

# 生产环境常见配置

代码语言:javascript
复制
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m

其中,MetaspaceSize是元空间触发 GC 的阈值,MaxMetaspaceSize是元空间的上限。

设置类元数据容量:对于需要加载大量类的应用,可以适当调大

-XX:CompressedClassSpaceSize=128m # 压缩类空间大小(默认1G)

2.4.2 优化代码避免类无限增长
  • 缓存动态生成的类:对于动态代理类,避免重复生成,进行缓存复用
代码语言:javascript
复制
// 改进:缓存代理类,避免重复生成
private static final Map<Class<?>, Class<?>> proxyClassCache = new ConcurrentHashMap<>();
public Class<?> getProxyClass(Class<?> targetClass) {
    return proxyClassCache.computeIfAbsent(targetClass, clazz -> {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> 
                            proxy.invokeSuper(obj, args));
        return enhancer.createClass();
    });
}
代码语言:javascript
复制

  • 避免频繁创建类加载器:类加载器本身会占用元空间,且其加载的类只有在类加载器被回收时才能被回收
  • 谨慎使用热部署:热部署通常通过创建新的类加载器实现,频繁热部署会导致元空间中积累大量类
  • 排查第三方库:某些 ORM 框架、序列化库或 AOP 框架可能会动态生成大量类,需检查其使用方式是否合理
  • 限制反射调用:过度使用反射可能导致 JVM 生成大量访问器类(如sun.reflect.GeneratedMethodAccessor),可通过系统属性限制: -Dsun.reflect.inflationThreshold=15 # 默认为15,超过此阈值会生成动态类

2.5 常见误区与最佳实践

  • 误区:元空间使用本地内存,因此不需要设置上限。 正解:虽然元空间使用本地内存,但仍需设置MaxMetaspaceSize,否则可能耗尽系统内存导致进程被杀死。
  • 误区:元空间溢出都是因为设置太小。 正解:多数情况下,元空间溢出是由于类泄漏(类加载器未回收导致类元数据无法回收),而非单纯的大小问题。
  • 最佳实践:在测试环境模拟类加载压力,观察元空间增长趋势,确定合理的元空间大小。
  • 最佳实践:对于使用大量动态代理的应用(如 Spring AOP),监控代理类的数量,确保其在合理范围内。

三、栈溢出(java.lang.StackOverflowError)

栈溢出虽然不是严格意义上的 OutOfMemoryError,但也是常见的内存相关错误。Java 虚拟机栈用于存储方法调用栈帧(包括局部变量、操作数栈、方法返回地址等),当方法调用深度超过栈的最大深度时,就会抛出StackOverflowError

3.1 栈溢出的典型场景

栈溢出最常见的原因是无限递归调用,此外,单个方法占用栈空间过大也可能导致栈溢出。

实战案例:无限递归导致的栈溢出

代码语言:javascript
复制
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * 演示栈溢出错误
 * 场景:无限递归调用
 */
public class StackOverflowDemo {
    private static final Logger logger = LoggerFactory.getLogger(StackOverflowDemo.class);
    private static int recursionDepth = 0;
    // 递归方法
    private static void recursiveMethod() {
        recursionDepth++;
        // 每1000层打印一次
        if (recursionDepth % 1000 == 0) {
            logger.info("递归深度:{}", recursionDepth);
        }
        // 无限递归调用
        recursiveMethod();
    }
    public static void main(String[] args) {
        try {
            logger.info("开始递归调用");
            recursiveMethod();
        } catch (StackOverflowError e) {
            logger.error("发生栈溢出,最大递归深度:{}", recursionDepth, e);
        }
    }
}
代码语言:javascript
复制


运行配置(可指定栈大小):

-Xss128k # 设置每个线程的栈大小为128KB(默认通常为1MB左右)

现象分析:程序中的recursiveMethod()不断调用自身,每次调用都会在虚拟机栈中创建一个新的栈帧。当调用深度超过栈的最大容量时,就会抛出栈溢出错误:

INFO StackOverflowDemo - 开始递归调用 INFO StackOverflowDemo - 递归深度:1000 INFO StackOverflowDemo - 递归深度:2000 ... ERROR StackOverflowDemo - 发生栈溢出,最大递归深度:3999 java.lang.StackOverflowError at StackOverflowDemo.recursiveMethod(StackOverflowDemo.java:18) at StackOverflowDemo.recursiveMethod(StackOverflowDemo.java:22) at StackOverflowDemo.recursiveMethod(StackOverflowDemo.java:22) ...

3.2 栈溢出的其他场景

除了无限递归,以下场景也可能导致栈溢出:

深层次的方法调用链:例如,在多层嵌套的框架中(如深层调用的 RPC、多层拦截器),即使没有递归,过深的调用链也可能导致栈溢出。

方法内创建大型数组:局部变量存储在栈帧中,创建大型数组会占用大量栈空间

// 可能导致栈溢出的代码

代码语言:javascript
复制
publicvoidlargeArrayInStack(){
// 在栈上分配大型数组(某些JVM可能优化到堆上,但理论上可能导致栈溢出)
int[] largeArray =newint[1000000];
}

复杂的表达式计算:某些复杂的表达式可能导致操作数栈深度超过限制

3.3 栈溢出的排查方法

栈溢出的排查相对直接,主要依靠错误堆栈信息:

  1. 分析异常堆栈StackOverflowError的堆栈信息会显示方法调用链,通过堆栈可以定位到递归或深层调用的位置。
  2. 检查递归逻辑:确认递归是否有正确的终止条件,终止条件是否能被触发。
  3. 测量调用深度:在递归或深层调用中添加计数器,记录实际调用深度,判断是否超出合理范围。
  4. 检查栈大小配置:如果确实需要较深的调用栈,可以适当调大栈大小(-Xss参数)。

3.4 栈溢出的解决方案

针对不同原因的栈溢出,解决方案如下:

3.4.1 解决无限递归问题

确保递归有正确的终止条件:检查递归的终止条件是否正确,是否存在逻辑错误导致无法终止

// 改进:添加正确的终止条件

代码语言:javascript
复制
privatestaticlongfactorial(int n){
// 终止条件
if(n <=1){
return1;
}
// 递归调用
return n *factorial(n -1);
}

将递归改为迭代:对于深度较大的递归,可改为循环迭代方式,避免栈溢出

// 改进:将递归改为迭代

代码语言:javascript
复制
privatestaticlongfactorialIterative(int n){
long result =1;
for(int i =2; i <= n; i++){
        result *= i;
}
return result;
}

增加递归深度限制:在递归方法中添加最大深度限制,防止无限递归

private static final int MAX_DEPTH=10000;

代码语言:javascript
复制

private static voidsafeRecursiveMethod(int depth){
if(depth >MAX_DEPTH){
        logger.warn("达到最大递归深度,终止递归");
return;
}
// 业务逻辑...
safeRecursiveMethod(depth +1);
}
3.4.2 解决深层调用链问题

优化调用链结构:减少不必要的中间层,简化方法调用链

使用尾递归优化:如果必须使用递归,尽量使用尾递归(递归调用是方法的最后一个操作),某些 JVM 会对尾递归进行优化(但 Java 标准 JVM 通常不支持)

适当增大栈空间:对于确实需要较深调用栈的场景(如深度嵌套的数据结构处理),可以适当调大线程栈大小

# 增大栈大小(根据需要调整)

代码语言:javascript
复制
-Xss512k
3.4.3 解决栈内存占用过大问题
  • 避免在方法内创建大型对象:大型对象应在堆上创建(使用 new 关键字),而非作为局部变量在栈上分配
  • 拆分大型方法:将包含大量局部变量的大型方法拆分为多个小方法,减少单个栈帧的大小

3.5 阿里巴巴开发规范中的相关要求

  • 【强制】递归调用必须有明确的终止条件。
  • 【建议】避免递归调用深度超过 1000 层。
  • 【建议】对于复杂的递归逻辑,考虑改为迭代实现。
  • 【建议】谨慎设置线程栈大小,过大的栈大小会导致可创建的线程数量减少。

四、无法创建新线程(java.lang.OutOfMemoryError: unable to create new native thread)

当 JVM 无法创建新的 native 线程时,会抛出unable to create new native thread错误。这通常不是因为 JVM 内存不足,而是由于系统限制或资源耗尽导致无法创建更多线程。

4.1 无法创建新线程的典型场景

这种 OOM 通常发生在以下情况:

  • 应用创建了过多线程,超过了系统限制
  • 系统资源不足(如内存),无法为新线程分配资源
  • 操作系统对进程的线程数量有限制,已达上限

实战案例:创建过多线程导致的 OOM

代码语言:javascript
复制
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
 * 演示无法创建新线程的OOM
 * 场景:无限制地创建新线程
 */
public class TooManyThreadsOOMDemo {
    private static final Logger logger = LoggerFactory.getLogger(TooManyThreadsOOMDemo.class);
    public static void main(String[] args) {
        List<Thread> threads = new ArrayList<>();
        int threadCount = 0;

        try {
            while (true) {
                // 创建新线程
                Thread thread = new Thread(() -> {
                    try {
                        // 线程创建后进入休眠,不释放资源
                        TimeUnit.HOURS.sleep(1);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });

                threads.add(thread);
                thread.start();
                threadCount++;

                if (threadCount % 100 == 0) {
                    logger.info("已创建 {} 个线程", threadCount);
                }
            }
        } catch (OutOfMemoryError e) {
            logger.error("无法创建新线程,已创建线程数:{}", threadCount, e);
        }
    }
}

现象分析:程序不断创建新线程并让它们进入长时间休眠,每个线程都会占用系统资源(包括内存和线程表项)。当线程数量达到系统限制时,JVM 无法创建新线程,抛出 OOM:

INFO TooManyThreadsOOMDemo - 已创建 100 个线程 INFO TooManyThreadsOOMDemo - 已创建 200 个线程 ... ERROR TooManyThreadsOOMDemo - 无法创建新线程,已创建线程数:32767 java.lang.OutOfMemoryError: unable to create new native thread at java.lang.Thread.start0(Native Method) at java.lang.Thread.start(Thread.java:717) at TooManyThreadsOOMDemo.main(TooManyThreadsOOMDemo.java:27)

不同系统和配置下,可创建的最大线程数不同,主要受以下因素影响:

  • 操作系统对每个进程的线程数限制(如ulimit -u在 Linux 上的设置)
  • 系统总内存大小(每个线程栈需要占用内存)
  • JVM 栈大小配置(-Xss参数,栈越大,能创建的线程越少)

4.2 线程创建的资源消耗

每个线程的创建都会消耗系统资源,主要包括:

  1. 线程栈内存:每个线程都有自己的栈空间,由-Xss参数控制,默认通常为 1MB 左右。创建 1000 个线程至少需要 1GB 内存(仅线程栈)。
  2. 操作系统资源:操作系统需要为每个线程维护线程控制块(TCB)等数据结构,消耗内核资源。
  3. 调度开销:过多的线程会增加 CPU 的调度开销,降低系统效率。

因此,线程并非越多越好,合理控制线程数量对系统稳定性至关重要。

4.3 无法创建新线程的排查方法

排查此类 OOM 需要结合系统配置和应用线程创建逻辑:

  1. 检查应用线程数量
    • 使用jstack <pid>查看当前线程数量和状态
    • 使用top -H -p <pid>查看进程的线程数
    • 使用 JVisualVM 的线程面板监控线程数量变化
  2. 分析线程创建来源
    • jstack输出中,查看线程名称和堆栈,确定哪些组件在创建线程
    • 检查是否有线程泄漏(线程创建后未正确终止)
  3. 检查系统限制
    • 在 Linux 上,使用ulimit -u查看用户最大进程数(线程在 Linux 上以轻量级进程实现)
    • 查看/proc/<pid>/limitsMax processes的限制
    • 检查系统总内存和剩余内存
  4. 检查 JVM 配置
    • 查看-Xss参数设置,过大的栈大小会减少可创建的线程数

4.4 无法创建新线程的解决方案

解决此类 OOM 的核心是控制线程数量优化线程使用

4.4.1 使用线程池管理线程

线程池是控制线程数量的最佳实践,它通过复用线程减少线程创建开销,同时限制最大线程数:

代码语言:javascript
复制
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
 * 改进:使用线程池管理线程
 */
public class ThreadPoolSolution {
    private static final Logger logger = LoggerFactory.getLogger(ThreadPoolSolution.class);

    // 推荐:使用ThreadPoolExecutor而非Executors创建线程池,明确参数含义
    private static final ExecutorService threadPool = new ThreadPoolExecutor(
        10, // 核心线程数
        100, // 最大线程数
        60L, TimeUnit.SECONDS, // 空闲线程存活时间
        new LinkedBlockingQueue<>(1000), // 任务队列
        new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
    );
    public static void main(String[] args) {
        try {
            int taskCount = 0;
            while (true) {
                taskCount++;
                final int taskId = taskCount;

                threadPool.submit(() -> {
                    try {
                        // 模拟任务执行
                        TimeUnit.SECONDS.sleep(1);
                        if (taskId % 100 == 0) {
                            logger.info("完成任务:{}", taskId);
                        }
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });

                // 控制任务提交速度,避免过快压满队列
                if (taskCount % 100 == 0) {
                    TimeUnit.MILLISECONDS.sleep(100);
                }
            }
        } catch (InterruptedException e) {
            logger.error("主线程中断", e);
            Thread.currentThread().interrupt();
        } finally {
            threadPool.shutdown();
        }
    }
}
4.4.2 合理配置线程池参数

根据业务场景合理配置线程池参数:

  • 核心线程数:保持在线的最小线程数,根据 CPU 核心数和任务类型调整
  • 最大线程数:控制线程池可创建的最大线程数,避免无限制增长
  • 任务队列:缓冲待执行的任务,减少线程创建需求
  • 拒绝策略:当线程池和队列都满时的处理策略,应根据业务需求选择(如 CallerRunsPolicy 可以起到限流作用)
4.4.3 避免线程泄漏

线程泄漏是指线程创建后无法正常终止,导致线程数量不断增加:

正确处理线程中断:线程应响应中断信号,及时退出

// 正确处理中断的线程

代码语言:javascript
复制
Thread worker =newThread(()->{
while(!Thread.currentThread().isInterrupted()){
try{
// 执行任务
doWork();
// 休眠时也能响应中断
TimeUnit.MILLISECONDS.sleep(100);
}catch(InterruptedException e){
// 重新设置中断状态
Thread.currentThread().interrupt();
break;
}
}
    logger.info("线程退出");
});

避免线程阻塞在无超时的操作上:如无超时的Object.wait()BlockingQueue.take()等,应使用带超时的版本

使用守护线程(Daemon Thread):对于后台服务线程,可设置为守护线程,在主线程退出时自动终止

Thread daemonThread =newThread(()->{

代码语言:javascript
复制
// 后台任务逻辑
});
daemonThread.setDaemon(true);// 设置为守护线程
daemonThread.start();
4.4.4 调整系统限制(谨慎操作)

在确实需要更多线程的情况下,可以适当调整系统限制(需谨慎,可能影响系统稳定性):

Linux 系统

# 临时调整用户最大进程数(线程数)

代码语言:javascript
复制
ulimit-u65535

# 永久调整(需要root权限)
echo"username soft nproc 65535">> /etc/security/limits.conf
echo"username hard nproc 65535">> /etc/security/limits.conf

减小线程栈大小:在确保不发生栈溢出的前提下,适当减小-Xss参数,增加可创建的线程数

-Xss256k # 将线程栈大小设置为256KB

4.5 阿里巴巴开发规范中的线程管理要求

【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。

【强制】线程池不允许使用 Executors 创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

【强制】线程池参数必须根据业务场景进行调整,核心线程数、最大线程数、队列大小等不能使用默认值。

【建议】线程池使用后必须正确关闭,避免资源泄漏。

【建议】线程池应命名,便于问题排查:

// 为线程池中的线程设置名称前缀

代码语言:javascript
复制
ThreadFactory namedThreadFactory =newThreadFactoryBuilder()
.setNameFormat("business-pool-%d").build();

ExecutorService pool =newThreadPoolExecutor(..., namedThreadFactory,...);

五、直接内存溢出(java.lang.OutOfMemoryError: Direct buffer memory)

直接内存(Direct Memory)是 Java NIO 提供的一种基于操作系统直接内存的缓冲区,它不在 JVM 堆内存中,而是使用本地内存。当直接内存分配超过限制时,会抛出Direct buffer memory的 OOM 错误。

5.1 直接内存溢出的典型场景

直接内存溢出通常发生在频繁使用 NIO 的场景,特别是:

  • 大量使用DirectByteBuffer且未及时释放
  • 直接内存大小设置过小,无法满足应用需求
  • 某些第三方库内部使用了直接内存,但存在内存泄漏

实战案例:大量分配直接内存导致的 OOM

代码语言:javascript
复制
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
 * 演示直接内存溢出
 * 场景:大量分配DirectByteBuffer且未释放
 */
public class DirectMemoryOOMDemo {
    private static final Logger logger = LoggerFactory.getLogger(DirectMemoryOOMDemo.class);
    public static void main(String[] args) {
        List<ByteBuffer> buffers = new ArrayList<>();
        int count = 0;

        try {
            // 每次分配1MB的直接内存
            int bufferSize = 1024 * 1024;

            while (true) {
                // 分配直接内存
                ByteBuffer buffer = ByteBuffer.allocateDirect(bufferSize);
                buffers.add(buffer);
                count++;

                if (count % 100 == 0) {
                    logger.info("已分配 {} 个直接缓冲区,共 {} MB", count, count);
                }
            }
        } catch (OutOfMemoryError e) {
            logger.error("发生直接内存溢出,已分配 {} 个缓冲区", count, e);
        }
    }
}

运行配置(限制直接内存大小):

-XX:MaxDirectMemorySize=100m # 限制直接内存最大为100MB

现象分析:程序不断分配 1MB 的直接内存缓冲区并保存引用,由于直接内存被限制为 100MB,当分配到约 100 个缓冲区时会抛出 OOM:

INFO DirectMemoryOOMDemo - 已分配 100 个直接缓冲区,共 100 MB ERROR DirectMemoryOOMDemo - 发生直接内存溢出,已分配 100 个缓冲区 java.lang.OutOfMemoryError: Direct buffer memory at java.nio.Bits.reserveMemory(Bits.java:694) at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123) at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311) at DirectMemoryOOMDemo.main(DirectMemoryOOMDemo.java:25)

5.2 直接内存的工作原理

直接内存与堆内存相比有以下特点:

  • 分配与回收:直接内存通过Unsafe.allocateMemory()分配,通过Unsafe.freeMemory()释放。DirectByteBuffer的回收依赖于 Cleaner(虚引用)机制,当DirectByteBuffer对象被 GC 回收时,其关联的 Cleaner 会触发直接内存的释放。
  • 性能特点:直接内存避免了 Java 堆和 native 堆之间的数据拷贝,适合频繁进行 IO 操作的场景(如网络通信、文件读写)。
  • 内存限制:直接内存默认受系统总内存限制,但可通过-XX:MaxDirectMemorySize参数指定上限。如果未指定,默认与堆内存最大值(-Xmx)相同。
  • OOM 触发:当申请直接内存时,如果预估分配后会超过MaxDirectMemorySize,会触发 Full GC 尝试回收直接内存。如果回收后仍不足,则抛出 OOM。

5.3 直接内存溢出的排查方法

直接内存溢出的排查相对复杂,因为它不在 JVM 堆内存中:

  1. 监控直接内存使用
    • 使用 JDK 自带的jconsolejvisualvm监控直接内存使用(需 JDK 8+)
    • 通过sun.misc.VM.maxDirectMemory()sun.misc.SharedSecrets.getJavaNioAccess().getDirectBufferPool().getTotalCapacity()获取直接内存限制和当前使用量
  2. 分析直接内存分配来源
    • 检查代码中使用ByteBuffer.allocateDirect()的地方
    • 排查使用 NIO 的第三方库(如 Netty、mina 等)
    • 使用内存分析工具(如 MAT)检查DirectByteBuffer对象的数量和引用情况
  3. 检查 GC 日志:直接内存分配前会触发 Full GC,查看 GC 日志中是否有频繁的 Full GC 且效果不佳
  4. 监控系统内存:使用topfree等工具监控系统整体内存使用,判断是否是系统内存耗尽导致

5.4 直接内存溢出的解决方案

解决直接内存溢出需要从合理使用和正确释放两方面入手:

5.4.1 合理配置直接内存大小

根据应用需求设置合适的直接内存上限:

# 根据实际情况调整,通常不需要超过物理内存的一半 -XX:MaxDirectMemorySize=512m

5.4.2 及时释放直接内存

主动释放直接内存:对于不再使用的DirectByteBuffer,可以通过反射调用其cleanerclean()方法主动释放

importsun.misc.Cleaner;

代码语言:javascript
复制
importjava.lang.reflect.Field;
importjava.nio.ByteBuffer;

// 主动释放直接内存的工具方法
publicstaticvoidfreeDirectBuffer(ByteBuffer buffer){
if(buffer.isDirect()){
try{
Field cleanerField = buffer.getClass().getDeclaredField("cleaner");
            cleanerField.setAccessible(true);
Cleaner cleaner =(Cleaner) cleanerField.get(buffer);
if(cleaner !=null){
                cleaner.clean();
}
}catch(Exception e){
            logger.error("释放直接内存失败", e);
}
}
}

避免长时间持有 DirectByteBuffer 引用:及时将不再使用的DirectByteBuffer对象置为 null,使其能被 GC 回收,进而触发直接内存释放

使用 try-with-resources:对于实现了AutoCloseable接口的缓冲区相关资源,使用 try-with-resources 确保释放

try(FileChannel channel =newFileInputStream("largeFile.txt").getChannel()){

代码语言:javascript
复制
ByteBuffer buffer =ByteBuffer.allocateDirect(1024*1024);
while(channel.read(buffer)!=-1){
// 处理数据
        buffer.clear();
}
}catch(IOException e){
    logger.error("文件处理错误", e);
}
5.4.3 优化直接内存使用

复用 DirectByteBuffer:对于频繁分配和释放直接内存的场景,使用对象池复用DirectByteBuffer

// 直接缓冲区对象池示例

代码语言:javascript
复制
publicclassDirectBufferPool{
privatefinalQueue<ByteBuffer> pool;
privatefinalint bufferSize;
privatefinalint maxSize;

publicDirectBufferPool(int bufferSize,int maxSize){
this.bufferSize = bufferSize;
this.maxSize = maxSize;
this.pool =newArrayDeque<>(maxSize);
}

// 从池中获取缓冲区
publicByteBufferborrowBuffer(){
ByteBuffer buffer = pool.poll();
return buffer !=null? buffer :ByteBuffer.allocateDirect(bufferSize);
}

// 将缓冲区归还给池
publicvoidreturnBuffer(ByteBuffer buffer){
if(buffer !=null&& buffer.capacity()== bufferSize && pool.size()< maxSize){
            buffer.clear();
            pool.offer(buffer);
}else{
// 不符合复用条件的缓冲区主动释放
freeDirectBuffer(buffer);
}
}
}

合理设置缓冲区大小:避免分配过大的直接缓冲区,根据实际需求调整

批量处理 IO 操作:减少 IO 操作次数,降低直接内存使用频率

5.4.4 排查第三方库

许多 IO 框架(如 Netty、Jetty 等)内部使用了直接内存,需确保其配置合理:

Netty:检查ByteBufAllocator配置,避免过度分配

// 合理配置Netty的内存分配器

代码语言:javascript
复制
ServerBootstrap bootstrap =newServerBootstrap();
bootstrap.option(ChannelOption.ALLOCATOR,PooledByteBufAllocator.DEFAULT);

调整框架内存参数:根据框架文档设置合理的内存上限和回收策略

5.5 直接内存与堆内存的选择

直接内存和堆内存各有优劣,应根据场景选择:

  • 使用直接内存的场景
    • 频繁进行 IO 操作(网络、文件)
    • 需要长期存在的大型缓冲区
    • 对性能要求高,希望避免堆和 native 堆之间的拷贝
  • 使用堆内存的场景
    • 内存使用量小
    • 生命周期短的对象
    • 对内存管理便利性要求高(依赖 GC 自动回收)

六、OOM 问题的通用排查与预防策略

无论哪种类型的 OOM,都有一些通用的排查方法和预防策略。掌握这些方法可以帮助我们快速定位问题并提前预防。

6.1 OOM 问题的通用排查流程

  1. 收集现场信息
    • 确保 JVM 参数中配置了-XX:+HeapDumpOnOutOfMemoryError,获取内存快照
    • 收集 OOM 发生时的应用日志和 JVM 日志
    • 记录 OOM 发生的时间点和当时的系统状态(CPU、内存、负载等)
  2. 初步分析
    • 根据 OOM 错误信息确定 OOM 类型(堆、元空间、栈、线程、直接内存)
    • 查看错误堆栈,定位可能的问题代码位置
    • 检查是否有明显的资源泄漏迹象(如大量相似对象、线程数异常等)
  3. 深入分析
    • 使用内存分析工具(MAT、JProfiler 等)分析内存快照
    • 结合代码审查,确认问题根源
    • 重现问题(在测试环境模拟 OOM 场景)
  4. 验证解决方案
    • 实施修复措施
    • 在测试环境验证,确保问题已解决
    • 监控修复后的内存使用情况

6.2 OOM 问题的预防策略

规范编码习惯

  • 遵循阿里巴巴开发规范,避免常见的内存泄漏风险点
  • 对集合、线程、IO 资源等进行正确管理
  • 定期进行代码审查,重点关注内存使用

合理配置 JVM 参数

  • 根据应用特性和服务器配置设置合适的 JVM 内存参数
  • 配置内存快照和 GC 日志参数,便于问题排查
  • 示例生产环境 JVM 配置:-Xms4g -Xmx4g \ -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m \ -XX:MaxDirectMemorySize=1g \ -Xss256k \ -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/heapdump.hprof \ -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:GCLogFileSize=100m -XX:NumberOfGCLogFiles=10 -Xloggc:/var/log/gc.log \ -XX:+UseG1GC -XX:MaxGCPauseMillis=200

实施监控告警

  • 使用 Prometheus+Grafana、Zabbix 等工具监控 JVM 内存使用
  • 设置内存使用阈值告警(如堆内存使用率超过 80%)
  • 监控 GC 频率和耗时,及时发现异常

压力测试与容量规划

  • 在上线前进行充分的压力测试,模拟高负载场景
  • 确定系统的内存使用基线和极限容量
  • 根据压力测试结果调整配置和优化代码

定期检查与优化

  • 定期分析内存快照和 GC 日志,发现潜在问题
  • 跟踪第三方库的更新,修复已知的内存泄漏问题
  • 根据业务增长情况调整资源配置

6.3 常用工具与命令

掌握以下工具和命令可以大大提高 OOM 问题的排查效率:

JDK 自带工具

  • jps:查看 Java 进程 ID
  • jstat:监控 JVM 统计信息(GC、类加载等)
  • jstack:获取线程堆栈信息
  • jmap:生成内存快照、查看内存使用统计
  • jconsole/jvisualvm:图形化监控工具

第三方分析工具

  • MAT(Memory Analyzer Tool):强大的内存快照分析工具,擅长查找内存泄漏
  • JProfiler:功能全面的 Java 性能分析工具,支持内存、CPU、线程等分析
  • YourKit:商业化的 Java 性能分析工具,内存分析功能强大

常用命令示例

# 查看Java进程

代码语言:javascript
复制
jps -l

# 监控GC情况(每5秒输出一次)
jstat -gcutil<pid>5000

# 生成内存快照
jmap -dump:format=b,file=heapdump.hprof <pid>

# 查看线程情况
jstack <pid>> threads.txt

# 查看类加载统计
jmap -clstats<pid>

七、总结:从踩坑到避坑的 OOM 实战指南

OOM 问题是 Java 开发中绕不开的挑战,但并非无法攻克。通过本文的系统学习,我们可以看到,每种 OOM 都有其特定的成因和解决方案:

  • 堆内存溢出:核心是避免内存泄漏和合理控制对象大小,通过内存快照分析可以有效定位问题对象。
  • 元空间溢出:通常与类加载相关,需关注动态类生成和类加载器管理。
  • 栈溢出:多由无限递归或过深调用链导致,将递归改为迭代是常用解决方案。
  • 无法创建新线程:通过线程池控制线程数量是最佳实践,避免无限制创建线程。
  • 直接内存溢出:需注意DirectByteBuffer的释放和复用,合理配置直接内存大小。

预防 OOM 问题的关键在于:

  1. 编写规范的代码,避免常见的内存管理陷阱
  2. 合理配置 JVM 参数,为不同类型的内存设置合适的上限
  3. 实施完善的监控告警机制,及时发现潜在问题
  4. 掌握必要的排查工具和方法,以便在 OOM 发生时快速定位和解决

最后需要强调的是,OOM 问题的排查是一个实践性很强的技能,需要结合理论知识和实际经验。在日常开发中,我们应该养成关注内存使用的习惯,定期进行代码审查和性能测试,将 OOM 问题消灭在萌芽状态。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-08-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 果酱带你啃java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、Java 堆内存溢出(java.lang.OutOfMemoryError: Java heap space)
    • 1.1 堆内存溢出的典型场景
      • 1.1.1 内存泄漏导致的 OOM
      • 1.1.2 内存溢出导致的 OOM
    • 1.2 堆内存溢出的排查方法
    • 1.3 堆内存溢出的解决方案
      • 1.3.1 解决内存泄漏问题
      • 1.3.2 解决内存溢出问题
    • 1.4 阿里巴巴开发规范中的相关要求
  • 二、方法区 / 元空间溢出(java.lang.OutOfMemoryError: Metaspace)
    • 2.1 元空间溢出的典型场景
    • 2.2 元空间的工作原理
    • 2.3 元空间溢出的排查方法
    • 2.4 元空间溢出的解决方案
      • 2.4.1 合理配置元空间参数
      • 2.4.2 优化代码避免类无限增长
    • 2.5 常见误区与最佳实践
  • 三、栈溢出(java.lang.StackOverflowError)
    • 3.1 栈溢出的典型场景
    • 3.2 栈溢出的其他场景
    • 3.3 栈溢出的排查方法
    • 3.4 栈溢出的解决方案
      • 3.4.1 解决无限递归问题
      • 3.4.2 解决深层调用链问题
      • 3.4.3 解决栈内存占用过大问题
    • 3.5 阿里巴巴开发规范中的相关要求
  • 四、无法创建新线程(java.lang.OutOfMemoryError: unable to create new native thread)
    • 4.1 无法创建新线程的典型场景
    • 4.2 线程创建的资源消耗
    • 4.3 无法创建新线程的排查方法
    • 4.4 无法创建新线程的解决方案
      • 4.4.1 使用线程池管理线程
      • 4.4.2 合理配置线程池参数
      • 4.4.3 避免线程泄漏
      • 4.4.4 调整系统限制(谨慎操作)
    • 4.5 阿里巴巴开发规范中的线程管理要求
  • 五、直接内存溢出(java.lang.OutOfMemoryError: Direct buffer memory)
    • 5.1 直接内存溢出的典型场景
    • 5.2 直接内存的工作原理
    • 5.3 直接内存溢出的排查方法
    • 5.4 直接内存溢出的解决方案
      • 5.4.1 合理配置直接内存大小
      • 5.4.2 及时释放直接内存
      • 5.4.3 优化直接内存使用
      • 5.4.4 排查第三方库
    • 5.5 直接内存与堆内存的选择
  • 六、OOM 问题的通用排查与预防策略
    • 6.1 OOM 问题的通用排查流程
    • 6.2 OOM 问题的预防策略
    • 6.3 常用工具与命令
  • 七、总结:从踩坑到避坑的 OOM 实战指南
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档