文章目录
结论:OOM异常也是Java异常的一种,默认情况下,如果是某个线程抛出异常,此线程会退出,并且异常堆栈会输出到控制台。如果JVM所有的非守护线程都因为OOM异常或者其他异常退出,那么JVM就会退出。
如果不确定什么是守护与非守护线程可查看:【JAVA】深入理解守护线程与非守护线程:概念、应用及示例
什么是OOM?
OOM(Out of Memory内存溢出)的缩写。它是一个计算机术语,用于描述计算机或设备在处理任务时由于内存不足而无法继续操作的状态
OOM的种类
- Java heap space: 当Java堆内存不足时抛出。
- PermGen space (在Java 8之前)或 Metaspace(在Java 8及之后): 当方法区内存不足时抛出。
- GC overhead limit exceeded: 当垃圾回收所花费的时间过多,且能回收的内存量很少。
- Direct buffer memory: 当尝试分配直接缓冲区的内存时失败。
JVM退出的条件
JVM在遇到OOM时是否退出,取决于以下几个因素:
-
异常类型 : 某些类型的OOM异常会导致JVM无法继续运行。例如,如果Java堆空间不足,可能会导致应用程序无法分配新的对象,进而引发
OutOfMemoryError
。如果在应用程序的关键部分没有进行适当的异常处理,JVM可能会因为未捕获的异常而退出。 -
异常处理 : 如果代码中捕获了
OutOfMemoryError
,并采取了相应的措施(例如释放一些资源、重试操作等),那么JVM可能不会退出。通过良好的异常处理,程序可以在内存不足的情况下继续运行。 -
线程和任务: 如果OOM发生在某个独立线程中,且主线程没有受到影响,JVM可能不会立刻退出。主线程可以继续执行,直到它决定结束。
-
JVM参数 : 某些JVM参数(如
-XX:+ExitOnOutOfMemoryError
)可以设置JVM在发生OOM时退出。如果没有特别配置,JVM可能会选择继续运行。
OOM示例
java
public class JvmThread {
public static void main(String[] args) {
new Thread(() -> {
List<byte[]> list = new ArrayList<byte[]>();
while (true) {
System.out.println(new Date().toString() + Thread.currentThread() + "==");
byte[] b = new byte[1024 * 1024 * 1];
list.add(b);
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
// 线程二
new Thread(() -> {
while (true) {
System.out.println(new Date().toString() + Thread.currentThread() + "==");
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
控制台打印信息
java
Wed Nov 07 14:42:18 CST 2018Thread[Thread-1,5,main]==
Wed Nov 07 14:42:18 CST 2018Thread[Thread-0,5,main]==
Wed Nov 07 14:42:19 CST 2018Thread[Thread-1,5,main]==
Wed Nov 07 14:42:19 CST 2018Thread[Thread-0,5,main]==
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
at com.gosaint.util.JvmThread.lambda$main$0(JvmThread.java:21)
at com.gosaint.util.JvmThread$$Lambda$1/521645586.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Wed Nov 07 14:42:20 CST 2018Thread[Thread-1,5,main]==
Wed Nov 07 14:42:21 CST 2018Thread[Thread-1,5,main]==
Wed Nov 07 14:42:22 CST 2018Thread[Thread-1,5,main]==
由此可见,在抛出oom之后,因为try-catch捕获后,控制台还是在打印,说吧jvm没有退出
堆内存变化图示
上图是JVM堆空间的变化。我们仔细观察一下在14:42:05~14:42:25之间曲线变化,你会发现使用堆的数量,突然间急剧下滑!
原因是:
Linux 内核有个机制叫OOM killer(Out Of Memory killer)
,该机制会监控那些占用内存过大,尤其是瞬间占用内存很快的进程,然后防止内存耗尽而自动把该进程杀掉。内核检测到系统内存不足、挑选并杀掉某个进程的过程可以参考内核源代码linux/mm/oom_kill.c
,当系统内存不足的时候,out_of_memory()
被触发,然后调select_bad_process()
选择一个bad进程杀掉。
如何判断和选择一个bad进程呢?
linux选择bad进程是通过调用oom_badness()
,挑选的算法和想法都很简单很朴实:最bad的那个进程就是那个最占用内存的进程。
那如果不捕获这个异常,jvm会做什么操作呢?
如果我们不处理异常,会有默认的java.lang.Thread.UncaughtExceptionHandler
处理。如果当前线程没有配置UncaughtExceptionHandler,会有线程组(ThreadGroup)
兜底处理(线程退出后,会将异常堆栈输出到控制台)
默认异常处理器原理
在Java中,异常处理是一个非常重要的概念。特别是在多线程环境中,处理未捕获的异常(即未被任何catch块捕获的异常)是保证程序稳定性和可靠性的一部分。
异常分类
Java中的异常主要分为两类:
- 检查异常(Checked Exception) :需要在方法签名中声明(使用
throws
),否则编译器会报错。比如IOException
、SQLException
等。 - 非检查异常(Unchecked Exception) :不需要在方法签名中声明,通常是程序错误,如
NullPointerException
、ArrayIndexOutOfBoundsException
等。它们是RuntimeException
及其子类的实例。
默认异常处理器
当线程中抛出未捕获的异常时,Java提供了一个默认的异常处理机制。这个机制的核心在于每个线程都有一个与之关联的默认异常处理器 。这个异常处理器是一个实现了Thread.UncaughtExceptionHandler
接口的对象。
如何设置默认异常处理器
可以通过Thread.setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler)
方法来设置当前线程的异常处理器。可以为每个线程或线程组设置特定的处理器,或者使用默认的处理器。
在 Thread 类中,处理未捕获异常的默认行为主要涉及以下几个方法:
- setUncaughtExceptionHandler(UncaughtExceptionHandler handler):设置当前线程的未捕获异常处理器。
- getUncaughtExceptionHandler():获取当前线程的未捕获异常处理器。
- 当线程运行过程中发生未捕获异常时,会调用 dispatchUncaughtException(Thread t, Throwable e)
方法来触发异常处理。
线程的异常处理机制
当一个线程抛出一个未捕获的异常时,Java虚拟机(JVM)会寻找以下两种处理机制:
-
当前线程的默认异常处理器 :首先,它会检查当前线程是否有设置默认的异常处理器。如果有,则调用这个处理器的
uncaughtException(Thread t, Throwable e)
方法。 -
线程的父线程的线程组:如果当前线程没有自定义的异常处理器,JVM会转而检查该线程所属的线程组的异常处理器。如果线程组有定义的处理器,则调用其处理方法。
下面是一个简单的例子,演示了如何使用默认异常处理器:
java
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
// 这将抛出一个未捕获的异常
throw new RuntimeException("这是一个未捕获的异常");
});
// 设置线程的默认异常处理器
thread.setUncaughtExceptionHandler((t, e) -> {
System.out.println("捕获到线程 " + t.getName() + " 的异常: " + e.getMessage());
});
thread.start();
}
}
在这个例子中,当thread
抛出RuntimeException
时,设置的默认异常处理器会捕获到该异常,并打印出异常信息,而不会导致程序崩溃。
线程组的作用
线程组在Java中提供了对一组线程的管理,除了可以统一管理线程的启动和停止外,线程组还可以处理未捕获的异常。这意味着,如果一个线程组中的任一线程抛出未捕获的异常,线程组的异常处理器会被调用。
示例代码
java
public class ThreadGroupExample {
public static void main(String[] args) {
ThreadGroup group = new ThreadGroup("MyThreadGroup");
Thread thread1 = new Thread(group, () -> {
throw new RuntimeException("Thread 1 exception");
});
Thread thread2 = new Thread(group, () -> {
throw new RuntimeException("Thread 2 exception");
});
// 设置线程组的默认异常处理器
group.setUncaughtExceptionHandler((t, e) -> {
System.out.println("捕获到线程组 " + group.getName() + " 的线程 " + t.getName() + " 的异常: " + e.getMessage());
});
thread1.start();
thread2.start();
}
}
在这个例子中,MyThreadGroup
中的任何线程抛出的未捕获异常都会被线程组的异常处理器捕获,并输出相应的信息。
总结
综上所述,JVM在遇到OOM时不一定会退出。具体情况取决于OOM的类型、程序中是否有适当的异常处理,以及JVM的配置。合理的异常处理和资源管理可以帮助程序在内存不足的情况下继续运行,避免意外退出。