工作中常见的6种OOM问题
堆内存OOM
堆内存OOM是最常见的OOM了。
出现堆内存OOM问题的异常信息如下:
java
java.lang.OutOfMemoryError: Java heap space
此OOM是由于JVM中heap的最大值,已经不能满足需求了。
举个例子:
java
@Test public void test01() {
List list = Lists.newArrayList();
while (true) {
list.add(new OOMTests());
}
}
这里创建了一个list集合,在一个死循环中不停往里面添加对象。
执行结果:

出现了java.lang.OutOfMemoryError: Java heap space的堆内存溢出。
很多时候,excel一次导出大量的数据,获取在程序中一次性查询的数据太多,都可能会出现这种OOM问题。
我们在日常工作中一定要避免这种情况。
栈内存OOM
有时候,我们的业务系统创建了太多的线程,可能会导致栈内存OOM。
出现堆内存OOM问题的异常信息如下:
java
java.lang.OutOfMemoryError: unable to create new native thread
举个例子
java
public class StackOOMTest {
public static void main(String[] args) {
while (true) {
new Thread().start();
}
}
}
使用一个死循环不停创建线程,导致系统产生了大量的线程。
如果实际工作中,出现这个问题,一般是由于创建的线程太多,或者设置的单个线程占用内存空间太大导致的。
建议在日常工作中,多用线程池,少自己创建线程,防止出现这个OOM。
栈内存溢出
我们在业务代码中可能会经常写一些 递归调用,如果递归的深度超过了JVM允许的最大深度,可能会出现栈内存溢出问 题。
出现栈内存溢出问题的异常信息如下:
java
java.lang.StackOverflowError
举个例子
java
@Test
public void test03() {
recursiveMethod();
}
public static void recursiveMethod() {
// 递归调用自身
recursiveMethod();
}

出现了java.lang.StackOverflowError栈溢出的错误。
我们在写递归代码时,一定要考虑递归深度。即使是使用 parentId一层层往上找的逻辑,也最好加一个参数控制递归 深度。防止因为数据问题导致无限递归的情况,比如:id和 parentId的值相等。
直接内存OOM
直接内存不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域。
它来源于 NIO ,通过存在堆中的 DirectByteBuffer 操作 Native内存,是属于堆外内存 ,可以直接向系统申请的内存空间。 出现直接内存OOM问题时异常信息如下:
java
java.lang.OutOfMemoryError: Direct buffer memory
例如:
java
private static final int BUFFER = 1024 * 1024 * 20;
@Test
public void test04() {
ArrayList<ByteBuffer> list = new ArrayList<>();
int count = 0;
try {
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
list.add(byteBuffer);
count++;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
System.out.println(count);
}
}
会看到报出来java.lang.OutOfMemoryError: Direct buffer memory直接内存空间不足的异常。
GC OOM
GC OOM 是由于JVM在GC时,对象过多,导致内存溢出,建 议调整GC的策略。 出现GC OOM问题时异常信息如下:
java
java.lang.OutOfMemoryError: GC overhead limit exceeded
为了方便测试,我先将idea中的最大和最小堆大小都设置成 10M,例如下面这个例子:
java
public class GCOverheadOOM {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executor.execute(() -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
}
});
}
}
}
出现这个问题是由于JVM在GC的时候,对象太多,就会报这 个错误。 我们需要改变GC的策略。 在老代80%时就是开始GC,并且将-XX:SurvivorRatio(- XX:SurvivorRatio=8)和-XX:NewRatio(- XX:NewRatio=4)设置的更合理。
元空间OOM
JDK8 之后使用 Metaspace 来代替 永久代 ,Metaspace是方 法区在 HotSpot 中的实现。
Metaspace不在虚拟机内存中,而是使用本地内存也就是在 JDK8中的 ClassMetadata ,被存储在叫做Metaspace的 native memory。
出现元空间OOM问题时异常信息如下:
java
java.lang.OutOfMemoryError: Metaspace
为了方便测试,我们修改一下idea中的JVM参数,增加下面的配 置:
java
-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
指定了元空间和最大元空间都是10M。 接下来,看看下面这个例子:
java
public class MetaspaceOOMTest {
static class OOM {
}
public static void main(String[] args) {
int i = 0;
try {
while (true) {
i++;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOM.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[]
return methodProxy.invokeSuper(o, args);
}
});
enhancer.create();
}
} catch (Throwable e) {
e.printStackTrace();
}
}
}
程序最后会报java.lang.OutOfMemoryError: Metaspace的 元空间OOM。 这个问题一般是由于加载到内存中的类太多,或者类的体积太 大导致的。
OOM一定会导致JVM退出吗
在Java中,发生了OutOfMemoryError(OOM)不一定会导致整个JVM退出。是否退出取决于发生OOM错误的线程和错误处理逻辑。这是一个复杂的问题,具体行为会因应用程序实现方式、错误发生的情境以及错误处理策略而异。
- 主线程中未处理的OOM: 如果在主线程中发生OOM且没有被捕获,JVM通常会终止程序并退出。这是因为JVM中没有其他存活的非守护线程来保持程序运行。
- 子线程中未处理的OOM: 在非主线程中,如果OOM发生且未被捕获,该线程会停止执行。但如果其他非守护线程仍在运行,JVM不会退出。
- 捕获并处理OOM: 如果在代码中捕获并正确处理了OOM错误,JVM则可以继续执行其余的程序代码。合适的错误处理可能包括释放内存资源或提示用户进行适当的操作。
注意:
- 不建议频繁捕获OOM并继续执行程序,因为这样可能表明程序有严重的内存管理问题,应尽量优化内存使用。
- 在关键路径中发生OOM时,通常应记录日志并考虑安全停机,因为无法保证系统在内存压力下的正确性。
垃圾回收调优的主要目标是什么?
分别是最短暂停时间和高吞吐量
- 最短暂停时间:垃圾回收调优的首要目标是减少应用程序的停顿时间,确保在垃圾回收过程中尽量保持应用的响应能力,特别是对于实时或高并发应用
- 高吞吐量:第二个目标是提高应用的吞吐量,即在单位时间内完成更多的业务处理,通过合理的GC策略和配置,减少GC的频率和时间,从而提升整体性
针对最短暂停时间和高吞吐举个例子:
- 方案一:每次 GC 停顿 100 ms,每秒停顿 5 次。
- 方案二:每次 GC 停顿 200 ms,每秒停顿 2次。
两个方案相对而言第一个时延低,第二个吞吐高,基本上两者不可兼得。所以调优时候需要明确应用的目标。
如何对 Java 的垃圾回收进行调优?
GC调优这种问题肯定是具体场景具体分析,但是在面试中就不要讲太细,大方向说清楚就行,不需要涉及具体的垃圾收集器比如 CMS 调什么参数,G1 调什么参数之类的。
GC 调优的核心思路就是尽可能的使对象在年轻代被回收,减少对象进入老年代。
具体调优还是得看场景根据 GC日志具体分析,常见的需要关注的指标是 Young GC 和 Ful GC 触发频率、原因、晋升的速率、老年代内存占用量等等。比加发现频繁产生FullGC,分析日志之后发现没有内存泄漏,只是 Young GC之后会有大量的对象进入老年代,然后最终触发FullGC,所以就能得知是 Survivor 空间设置太小,导致对象过早进入老年代,因此调大Survivor。或者是晋升年龄设置的太小,也有可能分析日志之后发现是内存泄漏、或者有第三方类库调用了 System.gc 等等。反正具体场景具体分析,核心思想就是尽量在新生代把对象给回收了,
基本上这样说就行了,然后就等着面试官延伸了
常用的JVM配置参数有哪些?
记住前两个,其它的使用时再查就行
JVM(Java虚拟机)的启动参数用于配置和调整Java应用程序的运行时行为。以下是一些常用的JVM启动参数:
- -Xmx:指定Java堆内存的最大限制。例如,-Xmx512m 表示最大堆内存为512兆字节。
- -Xms:指定Java堆内存的初始大小。例如,-Xms256m 表示初始堆内存为256兆字节。
- -Xss:指定每个线程的堆栈大小。例如,-Xss256k 表示每个线程的堆栈大小为256千字节。
- -XX:MaxPermSize(对于Java 7及之前的版本)或 -XX:MaxMetaspaceSize(对于Java 8及以后的版本):指定永久代(Java 7及之前)或元空间(Java 8及以后)的最大大小。
- -XX:PermSize(对于Java 7及之前的版本)或 -XX:MetaspaceSize(对于Java 8及以后的版本):指定永久代(Java 7及之前)或元空间(Java 8及以后)的初始大小。
- -Xmn:指定年轻代的大小。例如,-Xmn256m 表示年轻代大小为256兆字节。
- -XX:SurvivorRatio:指定年轻代中Eden区与Survivor区的大小比例。例如,-XX:SurvivorRatio=8 表示Eden区与每个Survivor区的大小比例为8:1。
- -XX:NewRatio:指定年轻代与老年代的大小比例。例如,-XX:NewRatio=2 表示年轻代和老年代的比例为1:2。
- -XX:MaxGCPauseMillis:设置垃圾回收的最大暂停时间目标。例如,-XX:MaxGCPauseMillis=100 表示垃圾回收的最大暂停时间目标为100毫秒。
- -XX:ParallelGCThreads:指定并行垃圾回收线程的数量。例如,-XX:ParallelGCThreads=4 表示使用4个线程进行并行垃圾回收。
- -XX:+UseConcMarkSweepGC:启用并发标记清除垃圾回收器。
- -XX:+UseG1GC:启用G1(Garbage First)垃圾回收器。
- -Dproperty=value:设置Java系统属性,可以在应用程序中使用 System.getProperty("property") 来获取这些属性的值。
这些是一些常见的JVM启动参数,可以根据应用程序的需求和性能调优的目标进行调整。JVM启动参数的使用可以显著影响应用程序的性能和行为,因此在设置这些参数时需要谨慎。同时,JVM支持的启动参数因不同的JVM版本和供应商而有所不同,建议查阅相关文档以获取更详细的信息。
你常用哪些工具来分析 JVM 性能?
- jmap:用于生成堆转储的命令行工具,可以用于分析JVM内存使用情况,尤其是内存泄漏问题
- jstack:用于生成线程转储的命令行工具,可以用于分析线程状态,排查死锁等问题
- jistat:用于监控JVM统计信息的命令行工具,提供了实时的性能数据,如类加载、垃圾回收、编译器等信息
- MAT:用于分析堆转储文件的工具,可以帮助识别内存泄漏和优化内存使用
- jconsole:可以监控JVM的内存使用、垃圾回收、线程、类加载等信息
- VisualVM:可实时显示 JVM 的内存使用、垃圾回收、类加载等信息,也可以分析 Heap Dump 等.
- Arthas:一个强大的Java 诊断工具,提供了实时监控和分析功能。通过命令行界面,可以查看 的状态、监控方法调用、追踪 SQL 查询、分析性能瓶颈等。
如何在 Java 中进行内存泄漏分析?
先确认是否真的发生了内存泄漏,即观察内存使用情况。
利用 jstat 命令(jstat -gc <pid> <interal in ms>
)来观察 gc 概要信息,如果发现,GC 后内存并没有明显的减少目还是持续增加持续触发gc,那说明内存泄漏的概率很大。
此时可以利用 jmap(jmap -dump:format=b,fi1e=heapdump.hprof <pid>
)生成 heap dump,然后将其导入 Ecdipse MAT 或者 VsuaVM 工具内进行分析,通过大量内存的占用可以找到对应的对象。
通过对象找到对应的代码分析,确认是否可能存在内存泄漏的场景,最终修复代码,解决内存泄漏的问题。