Java 虚拟机(JVM)发生 OutOfMemoryError(OOM)异常时,表示 JVM 在尝试分配内存时无法找到足够的内存资源。以下是几种常见的导致 OOM 异常的情况:
1. Java 堆空间不足 (Java Heap Space)
这种情况发生在 JVM 堆内存耗尽,无法再为新的对象分配空间。
原因
- 创建了大量对象且无法及时被垃圾回收。
- 内存泄漏:对象持有引用无法被垃圾回收。
- 内存中缓存过多数据。
解决方案
- 调整 JVM 堆内存大小(增加 -Xmx 参数)。
- 优化代码,减少内存消耗。
- 检查并修复内存泄漏。
Java 堆溢出排查解决思路
1.查找关键报错信息,比如 java.lang.OutOfMemoryError: Java heap space
2.使用内存映像分析工具(如Jprofiler)对Dump出来的堆储存快照进行分析,分析清楚是内存泄漏还是内存溢出。
这里给出我安装整合idea参考的教程
Intellij IDEA集成JProfiler性能分析神器-CSDN博客
3.如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,修复应用程序中的内存泄漏。
4.如果不存在泄漏,先检查代码是否有死循环,递归等,再考虑用 -Xmx 增加堆大小。 demo代码:
java
代码解读
复制代码
import java.util.ArrayList;
import java.util.List;
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
//在堆中无限创建对象
while (true) {
list.add(new OOMObject());
}
}
}
按照排除解决方案。
1.查找报错关键信息
arduino
代码解读
复制代码
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
2.使用内存映像分析工具Jprofiler分析产生的堆储存快照
(1)我们可以先通过 top -c查看当前服务器进程并记录当前消耗cpu最高线程的pid。
比如发现当前线程pid为744的使用率最高。
(2)然后通过下面的命令到处jvm内存快照
ini
代码解读
复制代码
jmap -dump:formart=b.file=java_pid_744.hprof 744
(java_pid_744.hprof是文件名。 744是通过top c查看消耗cpu使用率最高的线程id)
然后下载到本地,下载先可以先压缩一下,这样可以节省时间。一个小技巧。
(3)使用上面下载好的JProfiler打开生成的单个快照
OOMObject这个类创建了11956010个实例,是属于内存溢出
然后点击这个最大对象分析
然后我这时候电脑卡着了,借用网图给接下来步骤说明
打开后右键打开使用选定对象
然后这里会显示详细的日志
这里可以看见具体的代码块。然后我们就可以定位代码结合具体代码进行分析。发现死循环了。
2.线程栈空间不足 (Stack Overflow)
关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常;
- 如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出 OutOfMemoryError 异常。
原因
- 在单个线程下,栈帧太大,或者虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出StackOverflowError 异常。
- 不断地建立线程的方式会导致内存溢出。
解决方案
- 优化代码,避免过深的递归调用。
- 调整线程栈大小(增加 -Xss 参数)。
栈溢出排查解决思路
查找关键报错信息,确定是StackOverflowError还是OutOfMemoryError 如果是StackOverflowError,检查代码是否递归调用方法等 如果是OutOfMemoryError,检查是否有死循环创建线程等,通过-Xss降低的每个线程栈大小的容量
demo代码
typescript
代码解读
复制代码
public class JavaVMStackOOM {
private void dontStop() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
1.报错信息 Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
2.定位dontStop 方法是一个无限循环,线程一旦执行这个方法,将会一直循环下去
3.排查代码,确定是否显示使用死循环创建线程
3.方法区溢出
方法区,(又叫永久代,JDK8后,元空间替换了永久代),用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。运行时产生大量的类,会填满方法区,造成溢出。
方法区溢出原因
使用CGLib生成了大量的代理类,导致方法区被撑爆 在Java7之前,频繁的错误使用String.intern方法 大量jsp和动态产生jsp 应用长时间运行,没有重启
方法区溢出排查解决思路
调整元空间大小(增加 -XX:MaxMetaspaceSize 参数) 检查代码是否频繁错误得使用String.intern方法 优化类加载机制,减少不必要的类加载,检查是否使用CGLib生成了大量的代理类 重重启JVM
4.本机内存不足 (Native Memory Exhaustion)
这种情况发生在本机内存耗尽时。
原因
- 本机代码分配了大量内存(如 JNI 调用)。
- 内存泄漏。
解决方案
- 检查并优化本机代码。
- 确保本机内存使用合理。
比如: NIO程序中,使用ByteBuffer.allocteDirect(capability)分配的是直接内存,可能导致直接内存溢出。
ByteBuffer分配128MB直接内存,而JVM参数-XX:MaxDirectMemorySize=100M指定最大是100M,因此发生直接内存溢出。
5.GC 造成的内存不足 (GC Overhead Limit Exceeded)
这种情况发生在垃圾回收频繁且回收效果不明显时(超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。)。
原因
- 程序创建对象过快,垃圾回收无法跟上。
- 内存不足,垃圾回收无法有效清理。
解决方案
- 检查JVM参数-Xmx -Xms是否合理
- 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
- 增加 JVM 堆内存大小。
- 优化代码,减少对象创建速度。
- 使用更高效的垃圾回收器(如 G1 GC)。
demo
typescript
代码解读
复制代码
public class GCoverheadTest {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executor.execute(() -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
//do nothing
}
});
}
}
}
- 任务积压:线程池的大小是 10,这意味着同时最多只有 10 个任务在执行。其余的任务会被放入线程池的任务队列中等待执行。由于循环是无限的,任务会不断地被提交,导致任务队列不断增大。
- 内存消耗:随着任务队列中的任务越来越多,系统的内存消耗也会不断增加。最终,可能会导致内存耗尽,抛出 OutOfMemoryError 异常。
- 线程池饱和:线程池中的 10 个线程会不断地从任务队列中取任务执行,但由于每个任务都要休眠 10 秒钟,任务处理的速度远远跟不上任务提交的速度,导致任务队列越来越长。
总结
最后给出常见OOM异常
typescript
代码解读
复制代码
java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: unable to create new native thread
java.lang.OutOfMemoryError: Metaspace
java.lang.OutOfMemoryError: Direct buffer memory
java.lang.OutOfMemoryError: GC overhead limit exceeded