今天要探讨的是最近不知道为什么突然间火起来的面试题:当JAVA程序出现OOM之后,程序还能正常被访问吗?答案是可以的,很多时候他并不会直接导致程序崩溃,而是JVM会抛出一个error,告知你程序内存溢出了。当然也要分操作系统。
话不多说,直接上测试代码。测试代码分别从JVM堆溢出,栈溢出,以及直接内存测试一下,出现oom之后程序还能正常访问。
先定义一个正常测试用的接口:
@GetMapping("say")
public String say(){
return "say hello";
}
当各种情况内存溢出后,访问say接口看看是否能正常输出。
在《Java虚拟机规范》中,对虚拟机栈和本地方法栈规定了两类异常状况: 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常; 如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。 HotSpot虚拟机的栈容量是不可以动态扩展的。所以在HotSpot虚拟 机上是不会由于虚拟机栈无法扩展而导致OutOfMemoryError异常——只要线程申请栈空间成功了就不会有OOM,但是如果申请时就失败,仍然是会出现OOM异常的
堆内存溢出,只要定义一个全局变量,不断往里面添加元素,程序启动时候限制-Xmx大小一直让他溢出。
private List<byte[]> list = new ArrayList<>();
@GetMapping("oom")
public void oom(){
int i = 1;
while(i <= 5) {
byte[] bytes = new byte[1024 * 1024 * 500];
list.add(bytes);
System.out.println("第" + i + "次添加成功");
i++;
}
}
添加JVM启动参数,限制一下最大可用内存大小。
-Xmx100m -Xms100m
启动后访问http://localhost:8080/test/oom,控制台输出Java heap Space错误。
接着访问接口http://localhost:8080/test/say,接口正常输出。说明可以正常访问。
前面提到了,跟操作系统也会有关系。那么现在windows操作系统下,是可以正常访问的。我们切到Linux系统下测试。
请求http://localhost:8080/test/oom,控制台提示了Java heap space。
再访问http://localhost:8080/test/say,依然可以打印出say hello?嗯不对啊?网上很多都说linux有oom killer机制,那为什么这里还能访问?先留个疑问,我们再验证另外两种情况。
模拟栈溢出,只需要死循环一个递归即可。示例代码:
@GetMapping("sow")
public void sow(){
sow();
}
跟上面一样,windows启动访问http://localhost:8080/test/sow,出现栈溢出后,再次访问http://localhost:8080/test/say。
依然可以访问。
切换到Linux服务器上。
say接口还是可以访问。
Direct Buffer Memory为直接内存,一般在写IO程序(如Netty)的时候,经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(channel)和缓冲区(Buffer)的IO方式,他可以使用Native函数库直接分配对外内存,然后通过一个存储在java堆里面的DirectByteBuffer对象作为这块内存的引用操作,这样能在在一些场景中显著提高性能,因为避免了再java堆和Native堆中来回复制数据。
示例代码:
@GetMapping("dbm")
public void dbm(){
ByteBuffer buffer = ByteBuffer.allocateDirect(500 * 1024 * 1024);
}
windows系统下访问http://localhost:8080/test/dbm,出现异常后再访问http://localhost:8080/test/say。
依然可以访问。
切换到Linux系统。
丝毫不影响访问。
所以,经过测试后发现,出现了几种oom后,程序丝毫不影响啊。难道网上说的都是骗人的?感觉实际项目中出现oom后,程序也确实崩溃了呀,都得要重启。是不是有点慌了。
其实看似简单的一个是与否的问题,涉及的知识点包含了JVM的内存分配,作用域,GC等。其实发生OOM的线程一般情况下会死亡(注意是发生oom的线程),也就是会被终结掉,该线程持有的对象占用的heap都会被gc了,释放内存。因为发生OOM之前要进行gc,就算其他线程能够正常工作,也会因为频繁gc产生较大的影响。
那么肯定有人要问了,Linux不是有oom killer机制吗?那么请问上面linux模拟的几种情况依然可以访问,是不是oom killer被关闭了?我特地查了linux服务器的配置,并没有。
cat /proc/sys/vm/panic_on_oom
输出值默认是0,表示没有关闭。
我们先来了解一下OOM Killer 。OOM Killer 是内核中的一个进程,当系统出现严重内存不足时,它就会启用自己的算法去选择某一个进程并杀掉. 之所以会发生这种情况,是因为Linux内核在给某个进程分配内存时,会比进程申请的内存多分配一些. 这是为了保证进程在真正使用的时候有足够的内存,因为进程在申请内存后并不一定立即使用,当真正使用的时候,可能部分内存已经被回收了.。
Linux OOM Kill,这又分为两种: 一种是 cgroup 级别的:容器内所有进程使用的总内存超过了容器设置的内存上限,此时会触发该 cgroup 范围内的 OOM Kill(即在容器的进程中挑选进程杀掉),如果杀掉一个进程就可以满足,同时杀掉的进程不影响容器的 1 号进程运行,则容器就会继续运行; 一种是节点级别的:没有出现 cgroup OOM,但是整个操作系统的内存不足了,此时会在所有用户态进程中挑选进程进行 OOM kill;
那么,为什么会出现这种问题?它是如何产生的?OOM,全称为 “Out Of Memory”,即内存溢出。OOM Killer 是 Linux 自我保护的方式,防止内存不足时出现严重问题。
Linux 内核所采用的此种机制会时不时监控所运行中占用内存过大的进程,尤其针对在某一种瞬间场景下占用内存较快的进程,为了防止操作系统内存耗尽而不得不自动将此进程 Kill 掉。
通常,系统内核检测到系统内存不足时,筛选并终止某个进程的过程可以参考内核源代码:linux/mm/oom_kill.c,当系统内存不足的时候,out_of_memory()被触发,然后调用 select_bad_process() 选择一个 ”bad” 进程杀掉。
如何判断和选择一个”bad 进程呢?Linux 操作系统选择”bad”进程是通过调用 oom_badness(),挑选的算法和想法都很简单很朴实:最 bad 的那个进程就是那个最占用内存的进程。
因而程序就算出现了oom,但是还未达到整机内存无法分配时,是不会触发oom killer的。这个可以从系统日志也可以看到并没有oom killer相关的日志输出。
cat /var/log/messages
当物理内存和交换空间都被用完时,如果还有进程来申请内存,内核将触发OOM killer,其行为如下:
因此,不要再说oom后程序必然不能访问这么干脆的回答了。oom出现后,只是当前的线程因此出现oom而死亡,但其他线程依然是可以正常工作的。只有当系统中的物理内存和交换区都满了,系统无法为任何一个线程分配一个足够内存空间时,才会触发oom killer(仅限于linux系统,windows是没有oom killer机制的)进行bad进程的挑选,并强制停止。这其实也算是linux服务器本身的自我保护机制了。当然,对一个进程来说,内存的使用受多种因素的限制,可能在系统内存不足之前就达到了rlimit和memory cgroup的限制,同时它还可能受不同编程语言所使用的相关内存管理库的影响,就算系统处于内存不足状态,申请新内存也不一定会触发OOM killer,需要具体问题具体分析。