OOM异常类型总结

OOM是什么?英文全称为 OutOfMemoryError(内存溢出错误)。当程序发生OOM时,如何去定位导致异常的代码还是挺麻烦的。

要检查OOM发生的原因,首先需要了解各种OOM情况下会报的异常信息。这样能缩小排查范围,再结合异常堆栈、heapDump文件、JVM分析工具和业务代码来判断具体是哪些代码导致的OOM。笔者在此测试并记录以下几种OOM情况。

环境准备

  • jdk1.8(HotSpot虚拟机)
  • windows操作系统
  • idea开发工具

在idea上进行测试时,需要了解idea执行测试用例如何设置虚拟机参数(VM options)。如下图所示:

  1. 单击main方法的启动图标,选择修改运行配置

  2. 打开Add VM options,将JVM参数填在图示VM options处

堆溢出

Java堆是用来存储对象实例的,只要不断的创建对象,并保证对象不被GC回收掉,那么当对象占用的内存达到了最大堆内存限制,无法再申请到新的内存空间时,就会导致OOM。要让对象不被回收就需要保证GC Roots引用链可以到达该对象,此处采用了List来保持对对象的引用。并且设置参数-XX:+HeapDumpOnOutOfMemoryError打印OOM发生时的堆内存状态。代码如下:

复制代码
/**
 * VM options: -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
 * @author yywf
 * @date 2024/4/11
 */
public class HeapOOMTest {
    public static void main(String[] args) {
        List<Object> list = new LinkedList<>();
        while (true) {
            list.add(new Object());
        }
    }
}

执行结果

提示信息为GC overhead limit exceeded。

使用JProfiler打开heapDump文件,可以看到启动类加载器中的java.util.LinkedList占用了92.3%的堆内存

字符串常量池溢出

通过String.intern()这个native方法将字符串添加到常量池中。

测试代码如下:

复制代码
/**
 * VM options: -Xms2M -Xmx2M
 * @author yywf
 * @date 2024/4/11
 */
public class StringConstantOOMTest {
    public static void main(String[] args) {
        List<String> list = new LinkedList<>();
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}

执行结果

在jdk8中,字符串常量池已经移到了堆中。所以抛出的异常是堆内存溢出。

栈溢出

在JVM规范中,栈有虚拟机栈和本地方法栈之分。但在实际的实现中,HotSpot虚拟机是没有区分虚拟机栈和本地方法栈的。所以对于HotSpot来说,-Xoss(设置本地方法栈大小)参数是无效的,栈容量只能通过-Xss参数设置。

栈深度造成的溢出

在JVM规范中,如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。测试代码如下:

复制代码
/**
 * VM options: -Xss128k
 * @author yywf
 * @date 2024/4/11
 */
public class StackOOMTest {

    private int stackLength = 1;

    public void stackDeep() {
        stackLength++;
        stackDeep();
    }

    public static void main(String[] args) {
        StackOOMTest test = new StackOOMTest();

        try {
            test.stackDeep();
        } catch (Throwable e) {
            System.out.println("栈深度:" + test.stackLength);
            throw e;
        }
    }
}

执行结果

创建线程造成的内存溢出

另一种情况,机器的RAM内存是固定的,如果不考虑其他程序占用内存,那么RAM就由堆、方法区、程序计数器、虚拟机栈和本地方法栈瓜分。通过不断的创建线程占满RAM的内存,会导致什么情况呢?测试代码:

复制代码
/**
 * VM options: -Xss10M
 * @author yywf
 * @date 2024/4/11
 */
public class CreateThreadOOMTest {

    public void stackOOMByThread() {
        while (true) {
            Thread thread = new Thread(() -> {
                while (true) {
                    try {
                        Thread.sleep(60000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            thread.start();
        }
    }

    public static void main(String[] args) {
        CreateThreadOOMTest test = new CreateThreadOOMTest();
        test.stackOOMByThread();
    }
}

这里把栈的大小设置为了10M,也就是说创建一个线程最少需要10M的内存。可以更快的出现结果。
执行结果

抛出的是OutOfMemoryError。慎用慎用慎用,重要的事情说三遍,本人在测试的时候电脑死机了一会。得亏在线程的run方法中让线程睡眠了,不然cpu+内存双双阵亡。

方法区溢出

方法区大小在jdk1.7(包含)以前版本是通过**-XX:PermSize** 和**-XX:MaxPermSize** 来设置的。在jdk8的实现叫做元空间(metaspace),通过**-XX:MetaspaceSize=10M** 和**-XX:MaxMetaspaceSize=10M** 来设置其大小。

方法区存放的是类的信息,所以在运行时不断创建类就行。这里使用CGLib动态代理来生成类,可以添加以下maven依赖来使用CGLib:

复制代码
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>5.3.13</version>
</dependency>

测试代码如下:

复制代码
/**
 * VM options: -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
 * @author yywf
 * @date 2024/4/11
 */
public class MetaSpaceOomTest {

    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(Object.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                    return methodProxy.invokeSuper(o, args);
                }
            });
            enhancer.create();
        }
    }
}

执行结果

本机直接内存溢出

通过-XX:MaxDirectMemorySize=10M参数设置能申请的DirectMemory大小。如果不设置则默认为java堆的最大值。通过反射获取Unsafe实例,使用其来申请DirectMemory内存。

测试代码如下:

复制代码
/**
 * VM options: -Xmx10M -XX:MaxDirectMemorySize=10M
 * @author yywf
 * @date 2024/4/11
 */
public class DirectOOMTest {
    public static void main(String[] args) throws IllegalAccessException {

        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while (true) {
            unsafe.allocateMemory(1048576);
        }
    }
}

执行结果

相关推荐
风铃儿~1 分钟前
Spring AI 入门:Java 开发者的生成式 AI 实践之路
java·人工智能·spring
斯普信专业组7 分钟前
Tomcat全方位监控实施方案指南
java·tomcat
忆雾屿17 分钟前
云原生时代 Kafka 深度实践:06原理剖析与源码解读
java·后端·云原生·kafka
武昌库里写JAVA30 分钟前
iview Switch Tabs TabPane 使用提示Maximum call stack size exceeded堆栈溢出
java·开发语言·spring boot·学习·课程设计
gaoliheng00639 分钟前
Redis看门狗机制
java·数据库·redis
我是唐青枫41 分钟前
.NET AOT 详解
java·服务器·.net
Su米苏1 小时前
Axios请求超时重发机制
java
本郡主是喵2 小时前
并发编程 - go版
java·服务器·开发语言
南风lof2 小时前
源码赏析:Java线程池中的那些细节
java·源码阅读