本文首发于公众号:托尼学长,立个写 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)机制更加智能一些,它是通过终止占用内存最多的进程来保障系统稳定运行的。