前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >程序OOM后,还能正常访问吗?

程序OOM后,还能正常访问吗?

作者头像
有一只柴犬
发布2024-05-26 10:11:41
930
发布2024-05-26 10:11:41
举报
文章被收录于专栏:JAVA体系JAVA体系

1、前言

今天要探讨的是最近不知道为什么突然间火起来的面试题:当JAVA程序出现OOM之后,程序还能正常被访问吗?答案是可以的,很多时候他并不会直接导致程序崩溃,而是JVM会抛出一个error,告知你程序内存溢出了。当然也要分操作系统。

2、简单示例

话不多说,直接上测试代码。测试代码分别从JVM堆溢出,栈溢出,以及直接内存测试一下,出现oom之后程序还能正常访问。

先定义一个正常测试用的接口:

代码语言:javascript
复制
@GetMapping("say")
public String say(){
    return "say hello";
}

当各种情况内存溢出后,访问say接口看看是否能正常输出。

在《Java虚拟机规范》中,对虚拟机栈和本地方法栈规定了两类异常状况: 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常; 如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。 HotSpot虚拟机的栈容量是不可以动态扩展的。所以在HotSpot虚拟 机上是不会由于虚拟机栈无法扩展而导致OutOfMemoryError异常——只要线程申请栈空间成功了就不会有OOM,但是如果申请时就失败,仍然是会出现OOM异常的

2.1、Java Heap Space(堆内存溢出)

堆内存溢出,只要定义一个全局变量,不断往里面添加元素,程序启动时候限制-Xmx大小一直让他溢出。

代码语言:javascript
复制
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启动参数,限制一下最大可用内存大小。

代码语言:javascript
复制
-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机制,那为什么这里还能访问?先留个疑问,我们再验证另外两种情况。

2.2、StackOverFlowError

模拟栈溢出,只需要死循环一个递归即可。示例代码:

代码语言:javascript
复制
@GetMapping("sow")
public void sow(){
    sow();
}

跟上面一样,windows启动访问http://localhost:8080/test/sow,出现栈溢出后,再次访问http://localhost:8080/test/say。

依然可以访问。

切换到Linux服务器上。

say接口还是可以访问。

2.3、Direct Buffer Memory

Direct Buffer Memory为直接内存,一般在写IO程序(如Netty)的时候,经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(channel)和缓冲区(Buffer)的IO方式,他可以使用Native函数库直接分配对外内存,然后通过一个存储在java堆里面的DirectByteBuffer对象作为这块内存的引用操作,这样能在在一些场景中显著提高性能,因为避免了再java堆和Native堆中来回复制数据。

示例代码:

代码语言:javascript
复制
@GetMapping("dbm")
public void dbm(){
    ByteBuffer buffer = ByteBuffer.allocateDirect(500 * 1024 * 1024);
}

windows系统下访问http://localhost:8080/test/dbm,出现异常后再访问http://localhost:8080/test/say。

依然可以访问。

切换到Linux系统。

丝毫不影响访问。

2.4、结论

所以,经过测试后发现,出现了几种oom后,程序丝毫不影响啊。难道网上说的都是骗人的?感觉实际项目中出现oom后,程序也确实崩溃了呀,都得要重启。是不是有点慌了。

其实看似简单的一个是与否的问题,涉及的知识点包含了JVM的内存分配,作用域,GC等。其实发生OOM的线程一般情况下会死亡(注意是发生oom的线程),也就是会被终结掉,该线程持有的对象占用的heap都会被gc了,释放内存。因为发生OOM之前要进行gc,就算其他线程能够正常工作,也会因为频繁gc产生较大的影响。

那么肯定有人要问了,Linux不是有oom killer机制吗?那么请问上面linux模拟的几种情况依然可以访问,是不是oom killer被关闭了?我特地查了linux服务器的配置,并没有。

代码语言:javascript
复制
cat /proc/sys/vm/panic_on_oom

输出值默认是0,表示没有关闭。

3、关于OOM Killer

我们先来了解一下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相关的日志输出。

代码语言:javascript
复制
cat /var/log/messages

3.1、触发OOM Killer条件

当物理内存和交换空间都被用完时,如果还有进程来申请内存,内核将触发OOM killer,其行为如下:

  1. 检查文件/proc/sys/vm/panic_on_oom,如果里面的值为2,那么系统一定会触发panic
  2. 如果/proc/sys/vm/panic_on_oom的值为1,那么系统有可能触发panic(见后面的介绍)
  3. 如果/proc/sys/vm/panic_on_oom的值为0,或者上一步没有触发panic,那么内核继续检查文件/proc/sys/vm/oom_kill_allocating_task
  4. 如果/proc/sys/vm/oom_kill_allocating_task为1,那么内核将kill掉当前申请内存的进程
  5. 如果/proc/sys/vm/oom_kill_allocating_task为0,内核将检查每个进程的分数,分数最高的进程将被kill掉

4、小结

因此,不要再说oom后程序必然不能访问这么干脆的回答了。oom出现后,只是当前的线程因此出现oom而死亡,但其他线程依然是可以正常工作的。只有当系统中的物理内存和交换区都满了,系统无法为任何一个线程分配一个足够内存空间时,才会触发oom killer(仅限于linux系统,windows是没有oom killer机制的)进行bad进程的挑选,并强制停止。这其实也算是linux服务器本身的自我保护机制了。当然,对一个进程来说,内存的使用受多种因素的限制,可能在系统内存不足之前就达到了rlimit和memory cgroup的限制,同时它还可能受不同编程语言所使用的相关内存管理库的影响,就算系统处于内存不足状态,申请新内存也不一定会触发OOM killer,需要具体问题具体分析。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-05-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、前言
  • 2、简单示例
    • 2.1、Java Heap Space(堆内存溢出)
      • 2.2、StackOverFlowError
        • 2.3、Direct Buffer Memory
          • 2.4、结论
          • 3、关于OOM Killer
            • 3.1、触发OOM Killer条件
            • 4、小结
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档