Java内存溢出时,还能正常处理请求吗?
当你被问到“当Java程序发生内存溢出时,进程还能正常处理请求吗?”这样的面试题,会不会很懵?这里分享一次网友车辙在当初刚毕业那几年,意义风发,总觉得天下没有自己不会的面试题。然后在一次字节的面试中,彻彻底底的翻车的面试过程,希望提供大家一些面试经验。
Java 的优势有什么
面试官一上来,直接进入主题:你觉得在内存管理上,Java 有什么优势?
我:小菜一碟。相对于需要手动释放内存的 C 语言,Java 则通过垃圾回收机制实现了自动内存管理。这一机制能够自动辨识并清理不再使用的内存资源,从而省去了繁琐的手动释放过程,极大地简化了开发人员的工作。这让开发者能够将精力更专注地放在业务逻辑的构建上,而不必过多忧虑内存管理的问题,从而大大简化了开发人员的工作量。
什么是 OOM
面试官:请问您是否熟悉内存溢出(OOM)情况?
我:在我的经验中,我在线上经常遇到内存溢出的情况。特别是在使用Java编写的应用程序中,OOM通常是指内存溢出异常,即当应用程序需要为新对象分配内存空间时,可用内存不足以满足需求,从而导致了OOM异常的抛出。
什么情况会产生 OOM
面试官:好小子,线上的事故代码不会都是你写的吧,那你能谈谈是什么情况会导致内存溢出呢?
我:当然,一个常见的情况就是堆内存溢出。在创建对象时,大部分情况下都是占用 JVM 的堆内存。一旦堆内存无法满足对象的分配需求,就会抛出OOM异常。
错误信息通常是:java.lang.OutOfMemoryError: Java heap space
堆内存溢出的具体场景
面试官:你这个太抽象了,能不能具体点?
我:嗯,常见导致内存溢出的情况有这么几种:
对象生命周期过长:如果某个对象的生命周期过长,而且该对象占用的内存很大,那么在不断创建新对象的过程中,堆内存会被耗尽,从而导致内存溢出。这种情况一般出现在用集合当缓存,却忽略了缓存的淘汰机制。
无限递归:递归调用中缺少退出条件或递归深度过大,会导致空间耗尽,引发溢出错误。往往在测试环境就会发现该问题,不会暴露在生产环境
大数据集合:在处理大量数据时,如果没有正确管理内存,例如加载过大的文件、查询结果集过大等,会导致内存溢出。
JVM配置不当:如果JVM的内存参数配置不合理,例如堆内存设置过小,无法满足应用程序的内存需求,也会导致内存溢出。
下面的这个例子就是无限循环导致内存溢出。
什么是内存泄漏
面试官:你知道在我们的程序里,有可能会出现内存泄漏,你对它了解吗?
我:对的,和内存溢出的情况不同,还有一种特殊场景,叫做内存泄漏(本质上还是内存溢出,只不过是错误的内存溢出),指的是程序在运行过程中无法释放不再使用的内存,导致内存占用不断增加,最终耗尽系统资源,这种情况就被称为内存泄漏。
这一次,我提前抢答了, 常见导致内存泄漏的情况包括:
对象的引用未被正确释放:如果在使用完一个对象后,忘记将其引用置为 null 或者从数据结构中移除,那么该对象将无法被垃圾回收,导致内存泄漏。比如 ThreadLocal。
长生命周期的对象持有短生命周期对象的引用:如果一个长生命周期的对象持有了一个短生命周期对象的引用,即使短生命周期对象不再使用,由于长生命周期对象的引用仍然存在,短生命周期对象也无法被垃圾回收,从而造成内存泄漏。
过度使用第三方库:某些第三方库可能存在内存泄漏或者资源未正确释放的问题,如果使用不当或者没有适当地管理这些库,可能会导致内存溢出。
集合类使用不当:在使用集合类时,如果没有正确地清理元素,当集合不再需要时,集合中的对象也不会被释放,导致内存泄漏。
资源未正确释放:如果程序使用了诸如文件、数据库连接、网络连接等资源,在不再需要这些资源时没有正确释放,会导致资源泄漏,最终导致内存泄漏。
下面的这个例子就是长生命周期的对象持有短生命周期对象的引用, 导致内存泄漏。
List list2 = new ArrayList();
@GetMapping("/headOOM2")
public String headOOM2() throws InterruptedException {
while (true) {
list2.add(1);
}
}
还有其他情况吗
面试官:你说的都是堆的内存溢出,还有其他情况吗?
递归调用导致栈溢出
当递归调用的层级过深,栈空间无法容纳更多的方法调用信息时,会引发 StackOverflowError 异常,这也是一种 OOM 异常。例如,以下示例中的无限递归调用会导致栈溢出。
public class RecursiveExample {
public static void recursiveFunction() {
recursiveFunction();
}
public static void main(String[] args) {
recursiveFunction();
}
}
元空间(Metaspace)耗尽
元空间是 Java 8 及以后版本中用来存储类元数据的区域。它取代了早期版本中的永久代(PermGen)。元空间主要用于存储类的结构信息、方法信息、静态变量以及编译后的代码等。
当程序加载和定义大量类、动态生成类、使用反射频繁操作类等情况下,可能会导致元空间耗尽。常见导致元空间耗尽的情况包括:
类加载过多:如果应用程序动态加载大量的类或者使用动态生成类的方式,会导致元空间的使用量增加。如果无法及时卸载这些类,元空间可能会耗尽。
字符串常量过多:Java中的字符串常量会被存储在元空间中。如果应用程序中使用了大量的字符串常量,尤其是较长的字符串,可能会导致元空间的耗尽。
频繁使用反射:反射操作需要大量的元数据信息,会占用较多的元空间。如果应用程序频繁使用反射进行类的操作,可能会导致元空间耗尽。
大量动态代理:动态代理是一种使用反射创建代理对象的技术。如果应用程序大量使用动态代理,将会生成大量的代理类,占用较多的元空间。
未正确限制元空间大小:默认情况下,元空间的大小是不受限制的,它会根据需要动态扩展。如果没有正确设置元空间的大小限制,或者限制过小,可能会导致元空间耗尽。
下面的这个例子就是类加载过多导致的内存泄漏。
public class OOMExample {
public static void main(String[] args) {
while (true) {
ClassLoader classLoader = new CustomClassLoader();
classLoader.loadClass("com.example.LargeClass");
}
}
}
终极问题
面试官满意地点了点头,表示我对Java线程处理请求时的情况了解得很多。接着,他提出了一个问题:“当Java线程在处理请求时,发生了OOM异常,整个进程还能继续处理请求吗?”这个Java面试题可不简单哦!
在我正准备回答的时候,面试官却提醒我这个问题涉及的内容较为复杂,不是简单的是与否问题。他建议我先整理一下思路。
面试官的眼神透露出这道题目有些深意。经过一番思考,我给出了我的答案:“我认为OOM并不会导致整个进程崩溃。”
面试官随即追问:“你是怎么理解的?OOM难道不是内存不足的表现吗?既然内存不足,进程还能继续处理请求吗?”
我解释道:“尽管出现OOM,但通过垃圾回收机制仍有可能释放一些内存。”
面试官却反驳:“不是因为在垃圾回收后,发现内存依然不足才会抛出OOM异常吗?这难道不意味着垃圾回收已经无法继续执行,导致内存不足,进而触发了OOM异常吗?整个流程是内存不足,垃圾回收无效,最终OOM。”
我只能在内心默默吐槽,面对这样的组合拳,我有些措手不及。很遗憾,面试最终以失败告终。面试官在我离开时似乎在暗示:“这种情况我也不愿意发生,只能怪编程经验不够丰富。”
实战
回到家,我马上去进行了代码实战,用来测试 OOM。
环境是:OpenJdk 11 -Xms100m -Xmx100m -XX:+PrintGCDetails
堆内存溢出
首先我们创建一个方法,调用它,每隔一秒不停的循环打印控制台信息,它的主要作用是模拟其他线程处理请求。
@GetMapping("/writeInfo")
public String writeInfo() throws InterruptedException {
while (true) {
Thread.sleep(1000);
System.out.println("正在输出信息");
}
}
接着再创建一个死循环往 List 中放入对象的方法,它的主要作用是模拟导致OOM的那个线程。
@GetMapping("/headOOM")
public String headOOM() throws InterruptedException {
List list = new ArrayList();
while (true) {
list.add(1);
}
}
最终结果是headOOM
抛出了 OOM 异常,但是控制台还在不停的打印。【这边截图太大了,就不贴出来了】
这就是答案吗?其实不是,在第一步中,仅仅是在控制台打印出了日志,并没有创建明确的对象。将它稍微改动下,加一行,每次打印前先创建 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 。往下看
堆内存泄漏
老规矩,还是上面的方法
public String writeInfo() throws InterruptedException {
while (true) {
Thread.sleep(1000);
Byte[] bytes = new Byte[1024 * 1024 * 10];
System.out.println("正在输出信息");
}
}
创建一个内存泄漏的方法,list2 作用域是在类对象级别,从而产生内存泄漏
List list2 = new ArrayList();
@GetMapping("/headOOM2")
public String headOOM2() throws InterruptedException {
while (true) {
list2.add(1);
}
}
然后继续执行,结果首先是headOOM2
这个方法对应的线程抛出 OOM。
接着是 WriteInfo
这个方法对应的线程抛出OOM,所以我猜测现在整个进程基本都不能处理请求了。
为了印证这个猜测,再去调用下 writeInfo
这个方法,直接抛出 OOM 异常。说明我们的猜测是对的。
这时候你如果把那个 10M 改成1M,writeInfo
这个方法就又能执行下去了,不信的话就去试试看吧。
这说明内存泄漏的情况,其他线程能否继续执行下去,取决于这些线程的执行逻辑是否会占用大量内存。
不发生内存泄漏的情况下,为什么频繁创建对象会导致OOM,GC 不是会把对象给回收吗
- 堆内存限制:Java程序的堆内存有大小限制。如果频繁创建对象且无法及时回收,堆空间可能被耗尽。垃圾回收器会尝试回收不再使用的对象,但若创建速度超过回收速度,堆内存不足将导致OOM。
- 垃圾回收开销:尽管垃圾回收器回收不再使用的对象,但垃圾回收本身消耗时间和计算资源。频繁创建临时对象会增加回收时间,降低应用程序效率。
- 内存碎片化:频繁创建和销毁对象会导致内存空间碎片化。即使总的空闲内存足够,若没有足够的连续大块内存分配给新对象,也会发生OOM。
通过观察GC日志,可能发现OOM发生时,堆大小未达到阈值,进一步说明其他因素导致了OOM异常。
总结
首先,我们为你阐述了何为OOM以及其发生场景,包括内存溢出和内存泄漏,然后引出了问题:当Java线程在处理请求时,抛出OOM异常,整个进程是否仍能处理请求。
随后,我们进行了代码实验,模拟了内存溢出和内存泄漏两种情况,得出以下结论:
- 内存溢出情况下,若GC速度跟不上内存分配速度,导致OOM并杀死线程,通常整个进程仍能继续处理请求。
- 内存泄漏情况下,未能回收的内存可能引发OOM,继而杀死线程以防止无法回收对象继续产生。此时,那些不占用大量内存的线程可能会继续执行,但那些耗费大量内存的线程可能会无法执行。极端情况下,进程可能会崩溃。
以上结论反映了OOM对进程的影响,并指出了在不同情况下,进程是否仍能处理请求。
Java内存溢出时,还能正常处理请求吗?的更多相关文章
- java内存溢出和内存泄露
虽然jvm可以通过GC自动回收无用的内存,但是代码不好的话仍然存在内存溢出的风险. 最近在网上搜集了一些资料,现整理如下: —————————————————————————————————————— ...
- Java内存溢出的详细解决方案
本文介绍了Java内存溢出的详细解决方案.本文总结内存溢出主要有两种情况,而JVM经常调用垃圾回收器解决内存堆不足的问题,但是有时仍会有内存不足的错误.作者分析了JVM内存区域组成及JVM设置虚拟内存 ...
- 老李案例分享:定位JAVA内存溢出
老李案例分享:定位JAVA内存溢出 poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.在poptest的loadrunner的培 ...
- Java内存溢出分析方法(Eclipse Memory Analyzer 使用简单入门)
转载至:http://outofmemory.cn/java/jvm/OutOfMemoryError-analysis 工具 安装Memory Analyse Tools(MAT) 工具, 可以直接 ...
- java内存溢出问题
相信有一定java开发经验的人或多或少都会遇到OutOfMemoryError的问题,这个问题曾困扰了我很长时间,随着解决各类问题经验的积累以及对问题根源的探索,终于有了一个比较深入的认识. 在解决j ...
- Java内存溢出和内存泄露后怎么解决
1.首先这里先说一下内存溢出和内存泄露的区别: 内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory:比如申请了一个integer,但 ...
- Java内存溢出优化性能优化
高性能应用构成了现代网络的支柱.LinkedIn有许多内部高吞吐量服务来满足每秒数千次的用户请求.要优化用户体验,低延迟地响应这些请求非常重要. 比如说,用户经常用到的一个功能是了解动态信息——不断更 ...
- java内存溢出的解决思路
原文地址:https://www.cnblogs.com/200911/p/3965108.html 内存溢出是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能 ...
- [转]Java内存溢出详解及解决方案
原文地址:http://blog.csdn.net/xianmiao2009/article/details/49254391 内存溢出与数据库锁表的问题,可以说是开发人员的噩梦,一般的程序异常,总是 ...
- Java基础学习总结(30)——Java 内存溢出问题总结
Java中OutOfMemoryError(内存溢出)的三种情况及解决办法 相信有一定java开发经验的人或多或少都会遇到OutOfMemoryError的问题,这个问题曾困扰了我很长时间,随着解决各 ...
随机推荐
- json函数
Python与JSON(load.loads.dump.dumps) 1.Python中加载JSON 使用loads(string):作用将string类型转为dict字典或dict链表 # 加载 ...
- pywin32和wmi的安装和测试
E:\pyAPP\Madking\MadKingClient>python bin\NedStark.py collect_dataE:\pyAPP\Madking\MadKingClientT ...
- 微软Build 2023两大主题:Copilots和插件
在本周大型微软人工智能 2023 开发者大会的开幕式上,人工智能站到了舞台中央--前台和后台以及介于两者之间的所有舞台. 贯穿会议的两个主要主题是Copilots - 涵盖广泛产品和服务的AI助手 - ...
- WPF 自定义控件 二次渲染 问题记录
问题 将多个自定义控件加载到到一个页面的Grid上显示.然后突然将一个控件从Grid里面清除,控件依然在后台处理数据. 过段时间再加入Grid.然后一些已经改变的页面属性就消失了. 原因 经过查找是一 ...
- 代码随想录算法训练营Day18 二叉树
代码随想录算法训练营 代码随想录算法训练营Day18 二叉树| 513.找树左下角的值 112. 路径总和 113.路径总和ii 106.从中序与后序遍历序列构造二叉树 105.从前序与中序遍历序列构 ...
- 基于.NetCore开发博客项目 StarBlog - (28) 开发友情链接相关接口
前言 之前介绍的友情链接功能,只实现了友情链接的展示和管理接口. 还缺失友情链接申请.审核管理.通知,现在把这块功能补全. Model 什么的之前那篇文章都有,本文直接补全逻辑代码~ 详见: 基于.N ...
- 「学习笔记」DP 学习笔记1
序列 DP 一般序列 DP 核心思想:将序列的前 \(i\) 个数的状态用一个更简单的形式表示出,并且体现出这些状态对后续的影响. 题目 ABC 267D 给定一个序列 \(a\),找到一个长度为 \ ...
- ASP.NET Core 6框架揭秘实例演示[37]:重定向的N种实现方式
在HTTP的语义中,重定向一般指的是服务端通过返回一个状态码为3XX的响应促使客户端像另一个地址再次发起请求,本章将此称为"客户端重定向".既然有客户端重定向,自然就有服务端重定向 ...
- 【Unity3D】魔方
1 需求实现 绘制魔方 中基于OpenGL ES 实现了魔方的绘制,实现较复杂,本文基于 Unity3D 实现了 2 ~ 10 阶魔方的整体旋转和局部旋转. 本文完整代码资源见→基于 Unit ...
- 高效处理报表,掌握原生JS打印和导出报表为PDF的顺畅技巧!
摘要:本文由葡萄城技术团队于博客园原创并首发.转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 前言篇 在日常工作中,报表打印和导出为PDF是经常要处理的任务 ...