面试官:发生OOM后,JVM还能运行吗?

本文首发于公众号:托尼学长,立个写 1024 篇原创技术面试文章的flag,欢迎过来视察监督~

这是一道非常有意思的面试题,虽然不是特别高频,但也时不时的也会有面试官问起来。

而95%候选人给出的答案都是错的,他们认为"发生OOM后,JVM也一定不能运行了",其实结果恰恰相反。

下面我们直接Show Me The Code,一起进行场景复现。

双子线程场景

首先,我们将IDEA中的最大内存数设置成16M,以便于我们后续进行代码实验。

然后我们先运行一段代码,看看设置是否生效。

java 复制代码
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class TestOOM {

    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<byte[]>();
        for (;;) {
            System.out.println(new Date().toString() + " OOM Thread is running!!!");
            byte[] b = new byte[1024 * 1024 * 1];
            list.add(b);
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

果然生效了,代码没运行多久就报堆内存溢出了。

接下来我们在代码中模拟双子线程场景,看看到底会出现什么情况。

java 复制代码
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class TestOOM {

    public static void main(String[] args) {

        new Thread(() -> {
            List<byte[]> list = new ArrayList<byte[]>();
            for (;;) {
                System.out.println(new Date().toString() + " OOM Thread is running!!!");
                byte[] b = new byte[1024 * 1024 * 1];
                list.add(b);
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(() -> {
            for (;;) {
                System.out.println(new Date().toString() + " Other Thread is running!!!");
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

如日志所示,当一个线程已经出现OOM的时候,另一个线程仍在继续运行着。

从jconsole工具中可以看到,堆内存占用随着一个线程中的ArrayList不断填充对象而升高,高到xmx16M阈值的时候发生OOM并急转直下,随后趋于平稳。

这说明一个线程导致OOM的时候,该线程会终止运行并清空其所占用的内存资源,保证其他线程可以继续正常运行。

其实这种情况也是合理的,以现实生活为例,当写字楼电梯超载的时候,一定会让体重最重的那个胖子走下电梯,电梯还是要继续运行的。

主子线程场景

我们再来试试主子线程的场景,代码如下:

java 复制代码
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class TestOOM {

    public static void main(String[] args) {

        new Thread(() -> {
            List<byte[]> list = new ArrayList<byte[]>();
            for (;;) {
                System.out.println(new Date().toString() + " OOM Thread is running!!!");
                byte[] b = new byte[1024 * 1024 * 1];
                list.add(b);
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();

        for (;;) {
            System.out.println(new Date().toString() + " Master Thread is running!!!");
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

如日志所示,当子线程已经出现OOM的时候,该线程会终止运行并清空其所占用的内存资源,主线程并不会被影响,还是再持续运行着。

OOM线程判定

我们来思考一个比较有意思的问题,如果两个子线程都往ArrayList中存放数据,那如何判定是哪个线程导致的OOM呢?接下来继续用代码进行实现:

java 复制代码
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class TestOOM {

    public static void main(String[] args) {

        new Thread(() -> {
            List<byte[]> list = new ArrayList<byte[]>();
            for (;;) {
                System.out.println(new Date().toString() + " OOM Thread1 is running!!!");
                byte[] b = new byte[1024 * 1024 * 1];
                list.add(b);
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(() -> {
            List<byte[]> list = new ArrayList<byte[]>();
            for (;;) {
                System.out.println(new Date().toString() + " OOM Thread2 is running!!!");
                byte[] b = new byte[1024 * 1024 * 1];
                list.add(b);
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}
java 复制代码
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class TestOOM {
    public static void main(String[] args) {
        new Thread(() -> {
            List<byte[]> list = new ArrayList<byte[]>();
            for (;;) {
                System.out.println(new Date().toString() + " OOM Thread1 is running!!!");
                byte[] b = new byte[1024 * 1024 * 1];
                list.add(b);
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(() -> {
            List<byte[]> list = new ArrayList<byte[]>();
            for (;;) {
                System.out.println(new Date().toString() + " OOM Thread2 is running!!!");
                byte[] b = new byte[1024 * 512 * 1];
                list.add(b);
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

我们把这段代码反复执行,发现一个挺有趣的现象,线程1导致的OOM次数明显多一些,线程2导致的OOM次数少一些。随后我们把线程2往ArrayList中放512KB数据,改为了仅放128KB数据,线程2导致的OOM次数就更少了。真相原来是这样,哪个线程最后往ArrayList中存放数据导致OOM,成为压死骆驼的最后一根稻草,哪个线程就会被终止执行并清空其所占用的内存资源。

毕竟往ArrayList中写128KB数据导致最终OOM概率,远比往ArrayList中写1M数据最终导致OOM概率低很多。这样看来,还是Linux的OOM killer(Out Of Memory killer)机制更加智能一些,它是通过终止占用内存最多的进程来保障系统稳定运行的。

相关推荐
做运维的阿瑞3 小时前
Python零基础入门:30分钟掌握核心语法与实战应用
开发语言·后端·python·算法·系统架构
猿究院-陆昱泽3 小时前
Redis 五大核心数据结构知识点梳理
redis·后端·中间件
yuriy.wang4 小时前
Spring IOC源码篇五 核心方法obtainFreshBeanFactory.doLoadBeanDefinitions
java·后端·spring
咖啡教室6 小时前
程序员应该掌握的网络命令telnet、ping和curl
运维·后端
你的人类朋友7 小时前
Let‘s Encrypt 免费获取 SSL、TLS 证书的原理
后端
老葱头蒸鸡7 小时前
(14)ASP.NET Core2.2 中的日志记录
后端·asp.net
李昊哲小课7 小时前
Spring Boot 基础教程
java·大数据·spring boot·后端
码事漫谈7 小时前
C++内存越界的幽灵:为什么代码运行正常,free时却崩溃了?
后端
Swift社区8 小时前
Spring Boot 3.x + Security + OpenFeign:如何避免内部服务调用被重复拦截?
java·spring boot·后端
90后的晨仔8 小时前
Mac 上配置多个 Gitee 账号的完整教程
前端·后端