深入解析发生 OOM 的三大场景

在Java应用程序开发中,OutOfMemoryError(OOM)是一个令人头痛的问题。当JVM中的内存无法满足应用程序的需求时,就会抛出这个错误。本文将深入探讨OOM的三大场景:堆内存溢出、方法区内存溢出和栈内存溢出,并分析它们的原因,提供相应的实战解决方案。

什么是 OOM?

OOM 的全称是 Out Of Memory,那我们的内存区域有哪些会发生 OOM 呢?我们可以从内存区域划分图上,看一下彩色部分
在这里插入图片描述

可以看到除了程序计数器,其他区域都有OOM溢出的可能。但是最常见的还是发生在堆内存溢出、方法区内存溢出和栈内存溢出,主要是在堆上。

在这里插入图片描述

另外,我们常说的 OOM 异常,其实是 Error

在这里插入图片描述

一、堆内存溢出 ( Heap OOM )

Java 堆用于存储对象实例,我们只要不断的创建对象,并且保证 GC Roots 到对象之间有可达路径来避免 GC 清除这些对象,那随着对象数量的增加,总容量触及堆的最大容量限制后就会产生内存溢出异常。

Java 堆内存的 OOM 异常是实际应用中最常见的内存溢出异常。

/**
 * JVM参数:-Xmx10m
 */
public class JavaHeapSpaceDemo {
 
    static final int SIZE = 100 * 1024 * 1024;
 
    public static void main(String[] a) {
        int[] i = new int[SIZE];
    }
}

代码试图分配容量为 100M 的 int 数组,如果指定启动参数 -Xmx10m,分配内存就不够用,就类似于将 XXXL 号的对象,往 S 号的 Java heap space 里面塞。

原因分析

  1. 对象过多:应用程序创建了大量的对象,并且这些对象长时间存活,导致堆内存不足。
  2. 内存泄漏:应用程序中存在内存泄漏,即长时间无法释放不再使用的对象,导致堆内存持续占用。
  3. 超出预期的访问量/数据量:通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值

解决方案

针对大部分情况,通常只需要通过 -Xmx 参数调高 JVM 堆内存空间即可。如果仍然没有解决,可以参考以下情况做进一步处理:

  1. 优化代码:减少不必要的对象创建,避免过大的集合和数组。
  2. 如果是内存泄漏,需要找到持有的对象,修改代码设计,比如关闭没有释放的连接
  3. 如果是业务峰值压力,可以考虑添加机器资源,或者做限流降级。

二、栈内存溢出(Stack OOM)

栈内存溢出通常与线程的执行和递归调用有关。

public class StackOverflowErrorDemo {
 
    public static void main(String[] args) {
        javaKeeper();
    }
 
    private static void javaKeeper() {
        javaKeeper();
    }
}

原因分析

  1. 递归调用过深(最常见原因):递归算法实现不当,导致递归深度过大,超出了线程栈的大小限制。
  2. 线程创建过多:应用程序创建了大量的线程,并且每个线程的栈内存分配过多,导致系统资源耗尽。

解决方案

  1. 修复引发无限递归调用的异常代码, 通过程序抛出的异常堆栈,找出不断重复的代码行,按图索骥,修复无限递归 Bug

  2. 排查是否存在类之间的循环依赖

  3. 通过 JVM 启动参数 -Xss 增加线程栈内存空间, 某些正常使用场景需要执行大量方法或包含大量局部变量,这时可以适当地提高线程栈空间限制

三、方法区内存溢出(Metaspace OOM)

方法区内存溢出通常与类的加载和元数据的存储有关。

JDK 1.8 之前会出现 Permgen space,该错误表示永久代(Permanent Generation)已用满,通常是因为加载的 class 数目太多或体积太大。随着 1.8 中永久代的取消,就不会出现这种异常了。

Metaspace 是方法区在 HotSpot 中的实现,它与永久代最大的区别在于,元空间并不在虚拟机内存中而是使用本地内存,但是本地内存也有打满的时候,所以也会有异常。

/**
 * JVM Options: -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
 */
public class MetaspaceOOMDemo {
 
    public static void main(String[] args) {
 
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(MetaspaceOOMDemo.class);
            enhancer.setUseCache(false);
            enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> {
                //动态代理创建对象
                return methodProxy.invokeSuper(o, objects);
            });
            enhancer.create();
        }
    }
}

借助 Spring 的 GCLib 实现动态创建对象

Exception in thread "main" org.springframework.cglib.core.CodeGenerationException: java.lang.OutOfMemoryError-->Metaspace

方法区溢出也是一种常见的内存溢出异常,在经常运行时生成大量动态类的应用场景中,就应该特别关注这些类的回收情况。这类场景除了上边的 GCLib 字节码增强和动态语言外,常见的还有,大量 JSP 或动态产生 JSP 文件的应用(远古时代的传统软件行业可能会有)、基于 OSGi 的应用(即使同一个类文件,被不同的加载器加载也会视为不同的类)等。

原因分析

  1. 加载过多的类:每个类在加载到JVM时都会占用一定的方法区空间。如果程序加载了大量的类,那么方法区可能会被占满,导致OOM。
  2. 类加载器泄漏:如果类加载器没有正确地释放已经加载的类,那么这些类将一直占用方法区空间,导致方法区溢出。
  3. 动态生成类:在使用诸如JSP、反射或ASM等技术动态生成类时,如果生成过多的类或频繁地生成和卸载类,可能会导致方法区溢出

解决方案

  • 限制方法区大小:通过-XX:MaxMetaspaceSize参数设置方法区的最大值,避免无限制增长。这需要根据应用程序的实际情况进行调整。
  • 检查类加载器实现:确保自定义的类加载器正确实现了资源的释放,避免类加载器泄露。同时,注意检查和升级可能导致泄露的第三方库。
  • 优化类加载策略:按需加载和卸载类,避免不必要的类加载。可以考虑使用模块化技术(如OSGi)来管理类的加载和卸载。
  • 19
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值