如果面试被提到了OOM相关的问题,那么面试官主要在意的有哪些呢?
-
你有没有实际处理OOM的经验。
这是因为如果有这方面的经验,那么你在实际工作中会自觉规避这类问题,写出更高质量的代码
考察一个人是否有解决某类问题的经验的一个很重要角度就是他是否能准确描述出发生这类问题后的现象,你可能会忘记你是如何处理这个问题的,但是你一定忘不了那个被告警电话轰炸的那个晚上。
这也是上篇文章《美团一面,发生OOM了,程序还能继续运行吗?》想要帮大家解决的问题
-
考察你对OOM这方面的知识点了解的怎么样,俗称:八股取士。
当然,八股并不是一无是处,咱们也必须承认很多工作中碰到的问题的答案就藏在这些八股里面。今天就想通过这篇八股文跟大家聊聊OOM。咱们先说说OOM有哪几种,再讲讲在碰到OOM后如何快速定位,最后聊一聊定位了OOM如何修复,送一套组合拳给大家,不用谢!
一、OOM的分类
1. Java Heap Space OOM
这是最常见的OOM类型之一。堆内存不足时,JVM会抛出这个错误。
现象:应用程序占用的堆内存超出了最大限制,导致无法再分配内存。
示例代码:
import java.util.ArrayList;
import java.util.List;
// 为了方便复现,建议调整jvm参数:-Xmx100m
public class HeapOOM {
static class OOMObject {
// 占用一定内存的对象
private byte[] memory = new byte[1024 * 1024]; // 1MB
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}
运行这段代码,很快你就会看到java.lang.OutOfMemoryError: Java heap space
的提示。
咱们在平常开发的过程中,如果出现了OOM,99.99%的原因都是因为Java Heap Space OOM
,即:堆内存溢出。如果你碰到过非堆内存的溢出请告诉我,我愿意把这个概率调整为99.98%
2. GC Overhead Limit Exceeded
垃圾回收机制也会导致OOM哦。
原因:这个是JDK6新加的错误类型,一般都是堆太小导致的。Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。
解决方法
- 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
- 添加参数
-XX:-UseGCOverheadLimit
禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space。 - dump内存,检查是否存在内存泄露,如果没有,加大内存。
示例代码:
import java.util.HashMap;
import java.util.Map;
// 为了方便复现,建议调整jvm参数:-Xmx100m
public class GCOverheadOOM {
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>();
for (int i = 0; ; i++) {
map.put(i, new String(new char[10]));
}
}
}
这段代码会导致频繁的垃圾回收,最终抛出java.lang.OutOfMemoryError: GC overhead limit exceeded
错误。
实际工作中,基本不会碰到这个异常
3. PermGen Space OOM
永久代(或元空间)内存不足时,会抛出这个错误。
现象:永久代内存不足,导致加载类信息失败。
示例代码:
java
复制代码
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
public class PermGenOOM {
public static void main(String[] args) {
List<Class<?>> classes = new ArrayList<>();
// 无限循环,持续生成类
for (int i = 0; ; i++) {
try {
// 使用动态代理生成新的类
Class<?> proxyClass = Proxy.getProxyClass(
PermGenOOM.class.getClassLoader(),
MyInterface.class
);
classes.add(proxyClass);
System.out.println("Generated class: " + proxyClass.getName());
} catch (Exception e) {
e.printStackTrace();
break;
}
}
}
// 一个简单的接口,用来生成代理类
interface MyInterface {
void myMethod();
}
}
这段代码通过不断生成新的代理类,并将其添加到列表中,模拟了永久代内存的消耗,最终会导致java.lang.OutOfMemoryError: PermGen space
错误。
如果你在项目有有动态去生成类,那么你需要考虑到是否会创建太多的类而导致元空间内存溢出,一般情况下不需要考虑
4. Direct Buffer Memory OOM
直接内存不足,NIO操作失败时会抛出这个错误。
现象:直接内存不足,导致NIO操作失败。
示例代码:
java
复制代码
import java.nio.ByteBuffer;
public class DirectBufferOOM {
public static void main(String[] args) {
while (true) {
ByteBuffer.allocateDirect(1024 * 1024);
}
}
}
运行这段代码,最终会抛出java.lang.OutOfMemoryError: Direct buffer memory
。
二、如何定位OOM的原因
1.1 工具
要解决OOM问题,首先得知道问题出在哪里,咱们先介绍几个工具
- JVisualVM:这是JDK自带的监控工具,可以实时查看应用的内存使用情况、线程活动和堆快照等。
- JProfiler:这是一个强大的Java性能分析工具,能够详细分析内存使用、线程、GC等情况,适合进行深度分析。
- Eclipse MAT(Memory Analyzer Tool):这是一个强大的堆转储分析工具,能够帮助识别内存泄漏和大对象。
1.2 操作步骤
-
生成堆转储文件:当出现OOM时,可以生成堆转储文件供分析使用。
通常来说,我们都会配置jvm参数,当发生OOM后自动生成dump文件,参数如下:
# 设置当首次遭遇内存溢出时导出此时堆中相关信息 -XX:+HeapDumpOnOutOfMemoryError # 指定导出堆信息时的路径或文件名 -XX:HeapDumpPath=/tmp/heapdump.hprof
当然,有时候可能你忘记配置了这些参数,那么你可以通过执行以下命令
jmap -dump:live,format=b,file=heapdump.hprof 1234 # 1234代表java程序的pid,可以通过jps这个命令获取到
不过大部分情况当你去执行jmap命令时,程序已经崩溃了。这种情况我们应该怎么办呢?
一般来说如果问题发生的现场没有较好被保存的话,既没有日志,也没有堆dump文件,这个时候最好的办法就是加一些监控。
首先,你应该将JVM配置上,保证下次出现OOM时能够完整的保留dump文件,其次如果公司没有完善的监控平台的话,那么你可以通过一些shell脚本,定时的将堆dump文件保存下来,以便观察内存的变动情况,一般这个事情都是让公司的运维去做,我们只需要去分析dump文件就行了
-
分析堆dump文件:使用Eclipse MAT或JVisualVM打开堆转储文件,查看内存使用情况,找出占用内存最多的对象。
生成堆转储文件后,可以使用Eclipse MAT或JVisualVM进行分析。以下是使用Eclipse MAT的具体步骤:
-
- 启动Eclipse MAT
- 加载堆转储文件:在Eclipse MAT中,选择“File” > “Open Heap Dump”打开生成的
heapdump.hprof
文件。 - 分析内存使用情况:Eclipse MAT会自动分析堆转储文件,并生成一份内存使用报告。可以查看“Histogram”视图,找到占用内存最多的对象。
-
查看GC日志:通过分析GC日志,可以了解垃圾回收的频率和内存分配情况。启动应用时,添加如下参数以启用GC日志:
java -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause MyApplication
三、解决OOM的方案
实际上OOM最难的是定位,只要问题定位到了,解决起来都不麻烦。OOM说白了就是内存不够用了,不够用的原因可能是
- 浪费太多了,也就是内存溢出
- 用的方式不对,一次申请太多了,也就是创建了一些大对象
- 内存真不够用了,要加配置了
- 不想加配置咋办?想办法调调GC,找找更高效的回收算法
虽然我下面还做了一些分类,但是核心思路就是这4点
1.堆内存溢出
Java Heap Space OOM跟GC Overhead Limit Exceeded都归类到堆内存溢出
解决方案:
-
优化代码:
-
- 减少内存占用:尽量减少大对象的使用,合理规划对象的生命周期。
- 避免内存泄漏:确保不再使用的对象能被及时回收,例如关闭不再使用的连接、释放资源等。
-
调整JVM参数:
java -Xmx1024m MyApplication
-
-
Serial GC:适合单线程环境,暂停时间较长,但实现简单。
-
Parallel GC:适合多线程环境,吞吐量高,暂停时间较短。
-
CMS GC:并发标记-清除算法,适合低延迟应用,但会产生碎片。
-
G1 GC:垃圾优先收集器,适合大内存低延迟应用,能够均衡暂停时间和吞吐量。
-
调整GC策略:根据应用需求选择合适的垃圾回收策略。
java -XX:+UseSerialGC MyApplication
java -XX:+UseParallelGC MyApplication
java -XX:+UseConcMarkSweepGC MyApplication
java -XX:+UseG1GC MyApplication
-
增加堆内存:适当增加堆内存大小,例如通过
-Xmx
参数来调整最大堆内存。
-
2. 永久代、元空间溢出
解决方案:
-
减少类加载:尽量减少动态生成类和使用反射的次数。
-
调整永久代或元空间大小
java -XX:MaxMetaspaceSize=256m MyApplication
-
- 对于老版本的Java,可以通过
-XX:MaxPermSize
参数来调整永久代大小。对于Java 8及以上版本,使用-XX:MaxMetaspaceSize
参数调整元空间大小。
- 对于老版本的Java,可以通过
3.直接内存溢出
解决方案:
-
减少直接内存分配:尽量减少大块直接内存的分配,使用堆内存代替。
-
调整直接内存大小:通过
-XX:MaxDirectMemorySize
参数调整最大直接内存大小。
java -XX:MaxDirectMemorySize=256m MyApplication
四、总结
相信看完这两篇文章,大家对OOM的问题已经有了深入了解。在面试的时候,如果碰到这类问题,需要注意的是,我们首先要向面试官证明:我曾经碰到过OOM。
我们可以通过描述发生OOM后程序的一些表现,以及项目中发生OOM的原因来展示我们的经验。比如,我在《美团一面,发生OOM了,程序还能继续运行吗?》这篇文章中提到的真实经历:上线初期,数据量很少,我们一次性查询了所有数据。随着业务的增长,查询的数据量变得过多,导致了OOM。
当发生OOM时,我接到了告警电话。查看监控和代码后,很快确认是数据量过大导致的OOM。虽然出现了OOM告警,但也没有立刻修复。一方面,因为时间很晚;另一方面,因为跑批的线程挂掉后,程序的内存使用率就降下来了。所以当时只是暂时关闭了跑批任务,第二天到公司才进行处理。
这样的真实经历往往更能说服面试官,面试考察的不仅仅是技术,还有你在不同场景下的问题处理能力
其次,咱们可以补充一些具体的技术细节,比如如何获取到dump文件,如何使用JVisualVM或者Eclipse MAT进行内存分析,如何通过调整垃圾回收策略来优化内存管理等。