面试必杀题:当发生OOM时,进程还能处理请求吗

标签: 面试 oom 进程 | 发表时间:2023-05-31 14:38 | 作者:车辙cz
出处:https://juejin.im/backend?sort=monthly_hottest

大家好,我是车辙。还记得当初刚毕业那几年,意义风发,总觉得天下没有我不会的面试题。然后在一次字节的面试中,我 彻彻底底的翻车了。

Java 的优势有什么

面试官一上来,直接进入主题:你觉得在内存管理上,Java 有什么优势?

我:小菜一碟。相比于 C 语言的手动释放内存,Java 的优势在于 内存的自动管理,依赖于 垃圾回收机制,它能自动识别和清理不再使用的内存资源,消除了手动释放内存的繁琐过程,大大简化了开发人员的工作量。

什么是 OOM

面试官:那你知道什么是 OOM 吗?

我:这个我在线上也碰到过好多次了,Java 的 OOM 通常指的是 内存溢出(Out of Memory)异常。在 Java 应用程序中,每个对象都需要在内存中分配一定的空间。当应用程序需要分配更多内存空间来创建对象时,但可分配内存却不足以满足需求时,就会抛出 OOM 异常。

什么情况会产生 OOM

面试官:好小子,线上的事故代码不会都是你写的吧,那你说说有什么情况会导致 OOM?

我:比如说经常发生的 堆内存溢出, 在创建对象时,绝大多数情况占用的都是 JVM 的堆内存,当堆内存不足以分配时,则会抛出OOM异常。

java.lang.OutOfMemoryError: Java heap space

堆内存溢出的具体场景

面试官:你这个太抽象了,能不能具体点?

我:emm,常见导致内存溢出的情况有这么几种:

  1. 对象生命周期过长:如果某个对象的生命周期过长,而且该对象占用的内存很大,那么在不断创建新对象的过程中,堆内存会被耗尽,从而导致内存溢出。这种情况一般出现在用集合当缓存,却忽略了缓存的淘汰机制。
  2. 无限递归:递归调用中缺少退出条件或递归深度过大,会导致空间耗尽,引发溢出错误。往往在测试环境就会发现该问题,不会暴露在生产环境
  3. 大数据集合:在处理大量数据时,如果没有正确管理内存,例如加载过大的文件、查询结果集过大等,会导致内存溢出。
  4. JVM配置不当:如果JVM的内存参数配置不合理,例如堆内存设置过小,无法满足应用程序的内存需求,也会导致内存溢出。

下面的这个例子就是无限循环导致内存溢出。

  List<Integer> list = new ArrayList<>();
while (true) {
    list.add(1);
}

什么是内存泄漏

面试官:你知道在我们的程序里,有可能会出现内存泄漏,你对它了解吗?

我:对的,和内存溢出的情况不同,还有一种特殊场景,叫做内存泄漏( 本质上还是内存溢出,只不过是错误的内存溢出),指的是程序在运行过程中无法释放不再使用的内存,导致内存占用不断增加,最终耗尽系统资源,这种情况就被称为内存泄漏。

这一次,我提前抢答了, 常见导致内存泄漏的情况包括:

  1. 对象的引用未被正确释放:如果在使用完一个对象后,忘记将其引用置为 null 或者从数据结构中移除,那么该对象将无法被垃圾回收,导致内存泄漏。比如 ThreadLocal。
  2. 长生命周期的对象持有短生命周期对象的引用:如果一个长生命周期的对象持有了一个短生命周期对象的引用,即使短生命周期对象不再使用,由于长生命周期对象的引用仍然存在,短生命周期对象也无法被垃圾回收,从而造成内存泄漏。
  3. 过度使用第三方库:某些第三方库可能存在内存泄漏或者资源未正确释放的问题,如果使用不当或者没有适当地管理这些库,可能会导致内存溢出。
  4. 集合类使用不当:在使用集合类时,如果没有正确地清理元素,当集合不再需要时,集合中的对象也不会被释放,导致内存泄漏。
  5. 资源未正确释放:如果程序使用了诸如文件、数据库连接、网络连接等资源,在不再需要这些资源时没有正确释放,会导致资源泄漏,最终导致内存泄漏。

下面的这个例子就是 长生命周期的对象持有短生命周期对象的引用, 导致内存泄漏。

  List<Integer> list2 = new ArrayList<>();

@GetMapping("/headOOM2")
public String headOOM2() throws InterruptedException {
    while (true) {
        list2.add(1);
    }
}
image.png

还有其他情况吗

面试官:你说的都是堆的内存溢出,还有其他情况吗?

递归调用导致栈溢出

当递归调用的层级过深,栈空间无法容纳更多的方法调用信息时,会引发 StackOverflowError 异常,这也是一种 OOM 异常。例如,以下示例中的无限递归调用会导致栈溢出。

  public class OOMExample {
    public static void recursiveMethod() {
        recursiveMethod();
    }
    
    public static void main(String[] args) {
        recursiveMethod();
    }
}

元空间(Metaspace)耗尽

元空间是 Java 8 及以后版本中用来存储类元数据的区域。它取代了早期版本中的永久代(PermGen)。元空间主要用于存储类的结构信息、方法信息、静态变量以及编译后的代码等。

当程序加载和定义大量类、动态生成类、使用反射频繁操作类等情况下,可能会导致元空间耗尽。常见导致元空间耗尽的情况包括:

  1. 类加载过多:如果应用程序动态加载大量的类或者使用动态生成类的方式,会导致元空间的使用量增加。如果无法及时卸载这些类,元空间可能会耗尽。
  2. 字符串常量过多:Java中的字符串常量会被存储在元空间中。如果应用程序中使用了大量的字符串常量,尤其是较长的字符串,可能会导致元空间的耗尽。
  3. 频繁使用反射:反射操作需要大量的元数据信息,会占用较多的元空间。如果应用程序频繁使用反射进行类的操作,可能会导致元空间耗尽。
  4. 大量动态代理:动态代理是一种使用反射创建代理对象的技术。如果应用程序大量使用动态代理,将会生成大量的代理类,占用较多的元空间。
  5. 未正确限制元空间大小:默认情况下,元空间的大小是不受限制的,它会根据需要动态扩展。如果没有正确设置元空间的大小限制,或者限制过小,可能会导致元空间耗尽。

下面的这个例子就是 类加载过多导致的内存泄漏。

  public class OOMExample {
    public static void main(String[] args) {
        while (true) {
            ClassLoader classLoader = new CustomClassLoader();
            classLoader.loadClass("com.example.LargeClass");
        }
    }
}

终极问题

面试官满意的点了点头,小伙子你知道的还挺多,那我再问你一个问题哈:” 当 Java 线程在处理请求时,抛出了 OOM 异常,整个进程还能处理请求吗?

当我正准备脱口而出的时候,面试官:“这个问题考察的内容还是挺多的,不是简单的是与否的问题。我建议你先整理一下思路。”

看到面试官的眼神,我就知道这道题有猫腻。思考了一会,我给出了答案。“我还是认为OOM 并不会导致整个进程挂掉”

面试官:你是怎么理解的,OOM 是不是意味着内存不够了。既然内存不够了,进程还能处理请求吗?

我:内存不够了还可以通过垃圾回收释放内存。

面试官:难道 OOM 不就是因为 GC 后,发现内存不足才会抛出的异常,这时候是不是可以理解为 GC 不了了。所以是:内存不够->GC后还不够-> OOM 这个流程。

我:此处经典国骂,当然我只能在内心想想。

这么一套组合拳下来,我彻底懵了。结果不出意外的挂了,面试官最后送我下楼的时候,仿佛在和我说:”我也不想这样,只能怪 HC 太少“

实战

回到家,我马上去进行了代码实战,用来测试 OOM。

环境是:OpenJdk 11 -Xms100m -Xmx100m -XX:+PrintGCDetails

堆内存溢出

  1. 首先我们创建一个方法,调用它,每隔一秒不停的循环打印控制台信息,它的主要作用是 模拟其他线程处理请求
  @GetMapping("/writeInfo")
public String writeInfo() throws InterruptedException {
    while (true) {
        Thread.sleep(1000);
        System.out.println("正在输出信息");
    }
}
  1. 接着再创建一个死循环往 List 中放入对象的方法,它的主要作用是 模拟导致OOM的那个线程
  @GetMapping("/headOOM")
public String headOOM() throws InterruptedException {
    List<Integer> list = new ArrayList<>();
    while (true) {
        list.add(1);
    }
}
  1. 最终结果是 headOOM抛出了 OOM 异常,但是控制台还在不停的打印。【这边截图太大了,就不贴出来了】

  1. 这就是答案吗?其实不是,在第一步中,仅仅是在控制台打印出了日志,并没有创建明确的对象。将它稍微改动下,加一行, 每次打印前先创建 10M 的对象
  public String writeInfo() throws InterruptedException {
    while (true) {
        Thread.sleep(1000);
        Byte[] bytes = new Byte[1024 * 1024 * 10];
        System.out.println("正在输出信息");
    }
}

结果依旧会继续打印。看到这里有些人可能会说,答案确实是"还能继续执行",我只能说你是 Too Young Too Simple 。往下看

堆内存泄漏

  1. 老规矩,还是上面的方法
  public String writeInfo() throws InterruptedException {
    while (true) {
        Thread.sleep(1000);
        Byte[] bytes = new Byte[1024 * 1024 * 10];
        System.out.println("正在输出信息");
    }
}
  1. 创建一个 内存泄漏的方法,list2 作用域是在类对象级别,从而产生内存泄漏
  List<Integer> list2 = new ArrayList<>();
@GetMapping("/headOOM2")
public String headOOM2() throws InterruptedException {
    while (true) {
        list2.add(1);
    }
}
  1. 然后继续执行,结果首先是 headOOM2这个方法对应的线程抛出 OOM。

  1. 接着是 WriteInfo这个方法对应的线程抛出OOM,所以我猜测现在整个进程基本都不能处理请求了。

  1. 为了印证这个猜测,再去调用下 writeInfo这个方法,直接抛出 OOM 异常。说明我们的猜测是对的。

  1. 这时候你如果把那个 10M 改成1M, writeInfo 这个方法就又能执行下去了,不信的话就去试试看吧。

这说明内存泄漏的情况,其他线程能否继续执行下去,取决于这些线程的执行逻辑是否会占用大量内存。

不发生内存泄漏的情况下,为什么频繁创建对象会导致OOM,GC 不是会把对象给回收吗

最后再回答下这个问题:

  1. 堆内存限制:Java程序的堆内存有一定的大小限制,如果频繁创建对象并且无法及时回收,堆空间可能会被耗尽。虽然垃圾回收器会尽力回收不再使用的对象,但如果 对象创建的速度超过垃圾回收器的回收速度,就会导致堆内存不足而发生 OOM
  2. 垃圾回收的开销:尽管垃圾回收器会回收不再使用的对象,但 垃圾回收本身也是需要消耗时间和计算资源的。如果频繁创建大量的临时对象,垃圾回收器需要花费更多的时间来回收这些对象,导致应用程序的执行效率下降。
  3. 内存碎片化: 频繁创建和销毁对象会导致内存空间的碎片化。当内存中存在大量碎片化的空闲内存块时,即使 总的空闲内存足够,但可能无法找到连续的大块内存来分配给新对象。这种情况下,即使垃圾回收器回收了部分对象,仍然无法分配足够的内存给新创建的对象,从而导致OOM。 所以你可以从GC日志上发现,发生OOM时,你的堆大小没有到达你的阈值。

不知道到这,你看懂了没有。

总结

首先,我们铺垫了什么是 OOM,以及 OOM 发生的场景,包括内存溢出、内存泄漏,从而得出了这个问题: 当 Java 线程在处理请求时,抛出了 OOM 异常,整个进程还能处理请求吗?

接着通过代码实战,模拟了内存溢出和内存泄漏两个场景,暂时性的得出了结论:

  1. 内存溢出的情况,当 GC 的速度跟不上内存的回收时,会发生 OOM, 从而将那个线程 Kill 掉,在这种情况下,进程一般还能继续处理请求。
  2. 内存泄漏的情况,由于这些内存不能被回收掉,会发生OOM,从而将那个线程 Kill 掉,防止继续创建不能被回收的对象,此时有些不占用内存的线程可能将继续执行,而那些会占用大量内存的线程可能将无法执行,最坏的情况可能是进程直接挂掉。

如果这篇文章对您有所帮助,可以关注我的公众号《车辙的编程学习圈》,领取视频教程、电子书、面试攻略等海量资源。

我是车辙,掘金小册《SkyWalking》作者,一名常被HR调侃为XX杨洋的互联网打工人。欢迎大家点赞、评论、转发。

image.png

相关 [面试 oom 进程] 推荐:

面试必杀题:当发生OOM时,进程还能处理请求吗

- - 掘金后端本月最热
还记得当初刚毕业那几年,意义风发,总觉得天下没有我不会的面试题. 然后在一次字节的面试中,我 彻彻底底的翻车了. 面试官一上来,直接进入主题:你觉得在内存管理上,Java 有什么优势. 相比于 C 语言的手动释放内存,Java 的优势在于 内存的自动管理,依赖于 垃圾回收机制,它能自动识别和清理不再使用的内存资源,消除了手动释放内存的繁琐过程,大大简化了开发人员的工作量.

redis进程OOM被linux内核kill问题调查 - 简书

- -
运维人员收到zabbix告警说codis集群usa-9节点所在机器,原swap 4G 空间只剩下80k. 其立即登录该机器增加了约6G的swap空间. 接着收到某个应用的500错误告警,错误堆栈里提到codis该usa-9节点 “JedisConnectionException: Unexpected end of stream”,再次登录usa-9拿到 linux的系统日志如下:.

Android OOM案例分析

- - 美团点评技术团队
在Android(Java)开发中,基本都会遇到 java.lang.OutOfMemoryError(本文简称OOM),这种错误解决起来相对于一般的Exception或者Error都要难一些,主要是由于错误产生的root cause不是很显而易见. 由于没有办法能够直接拿到用户的内存dump文件,如果错误发生在线上的版本,分析起来就会更加困难.

高并发下的oom killer

- - 操作系统 - ITeye博客
最近在搞分布式批处理平台的项目,在进行压力测试的过程中出现oom killer,而且是在linux'系统日志抛出的;. 环境:VMware虚拟机(8c/16g/100g),并发线程数:16个,称此系统为A,在A系统处理的过程中需要调用B系统的服务,是通过http协议进行的调用;. 下面描述一下排查错误的过程及相关的知识,其中一些文章是转载的一下比较好的文章;.

Android 内存溢出解决方案(OOM)

- - CSDN博客移动开发推荐文章
众所周知,每个Android应用程序在运行时都有一定的内存限制,限制大小一般为16MB或24MB(视平台而定). 因此在开发应用时需要特别关注自身的内存使用量,而一般最耗内存量的资源,一般是图片、音频文件、视频文件等多媒体资源;由于Android系统对音频、视频等资源做了边解析便播放的处理,使用时并不会把整个文件加载到内存中,一般不会出现内存溢出(以下简称OOM)的错误,因此它们的内存消耗问题暂不在本文的讨论范围.

Linux的OOM killer简单测试

- - Linux - 操作系统 - ITeye博客
       顾名思义,OOM(out of memory) killer,是Linux操作系统发现内存不足时,它会强制杀死一些用户进程(非内核进程),来保证系统有足够的物理内存进行分配.     Linux对大部分申请内存的请求都回复"yes",以便能跑更多更大的程序. 因为申请内存后,并不会马上使用内存.

Node.js 内存溢出OOM分析

- -
Node.js 内存飙涨以及 OOM 的问题,只要业务流量稍微复杂,一般都会遇到. 如果是堆内内存,在 OOM 之前可以打一个 Heap Profiling 进行分析,如果是 OOM 之后,可以利用 llnode 对 corefile 进行分析,但如果是堆外内存飙涨呢. 这一块内存通过 Chrome Devtool 工具是分析不出来的.

菜鸟也能解决android中的OOM问题

- - CSDN博客移动开发推荐文章
只要你记住下面几个原则,在android 中处理图片的OOM问题绝对是easy之极:. 1.超大图片要按比例压缩之后才做显示,退出当前activity 必须回收. 关于inSampleSize 可根据自己的实际情况去定. 2.大图片(30~50k)的可直接显示,退出当前activity 立即回收. 3.大量的小图 或者不同size的图片要展示,请参看我的另外一篇LRU算法缓存图片的:http://blog.csdn.net/androidzhaoxiaogang/article/details/8211649.

Eclipse的Mat Plugin查找OOM使用一例

- - CSDN博客推荐文章
最近接手了一个老项目比较头痛. 头痛的原因是这个代码的编写者已经离开了公司,而且代码基本没有注释,结构混乱并且还有严重的内存泄漏问题. 其实接手这个项目最大需要解决的问题就是内存泄漏问题. 由于这个老项目使用JDK1.5,所以像JDK1.6自带很多内存检测工具都派不上用场了. 比如:jdk1.5 使用的jmap -heap 生成的dump文件用eclipse的mat就打不开.

OOM分析——错误使用Servlet API导致内存溢出

- - 开涛的博客
请先前往《 Spring内存溢出问题》查看问题,大体问题就是突然间内存飙升,且CPU使用率非常高. 通过内存dump分析发现内存中某个key会有几百万个,而且观察这些key会发现有时候是org.springframework.web.servlet.DispatcherServlet.LOCALE_RESOLVER,有时候又变成org.springframework.web.servlet.DispatcherServlet.THEME_RESOLVER,每次可能不一样.