JVM关闭的方式
正常退出
全部非Daemon线程执行结束
所有非守护线程执行结束后,JVM会自动退出。此时如果有Daemon线程,仍然会自动退出。
public class Exits {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName());
}
});
t.setName("foolBoy");
t.setDaemon(false);
t.start();
Thread daemon = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName());
}
});
daemon.setName("daemon");
daemon.setDaemon(true);
daemon.start();
}
}
System.exit(0)
Ctrl + c
事实上,ctrl + c是发送一个信号. (kill foreground process ) 发送 SIGINT 信号给前台进程组中的所有进程,强制终止程序的执行.
kill -15 (SIGTERM信号)
异常退出
Runtime&Error
OOM
强制关闭
Kill -9 SIGKILL信号
Runtime.halt
断电
系统关机
系统Crash
JVM安全退出
对于tomcat类Web应用,我们可以直接通过Runtime.addShutdownHook(Thread hook)注册自定义钩子,在钩子中实现资源的清理;而对于worker类应用,我们可以采用如下的方式安全的退出应用。
ShutdownHook
JVM正常退出或异常退出都会执行shutdownHook。其中shutdownHook是一个已初始化但并不启动的线程,当jvm关闭的时候,会执行系统中已经设置的所有通过方法addShutdownHook添加的钩子,当系统执行完这些钩子后,jvm才会关闭。所以可通过这些钩子在jvm关闭的时候进行内存清理、资源回收等工作。
用法
Runtime.getRuntime().addShutdownHook(Thread thread)
这里我们需要将一个线程对象传入,作为钩子程序的实现代码。本质上就是在jvm关闭时,执行一个线程。
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Clean Resource....");
}
}));
基于信号的进程通知机制
信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。通俗来讲,信号就是进程间的一种异步通信机制。Linux常见信号:
信号名称 | 用途 |
---|---|
SIGKILL(kill -9) | 终止进程,强制杀死进程 |
SIGTERM(kill -15) | 终止进程,软件终止信号 |
SIGINT(ctrl-c) | 终止进程,中断进程 |
ShutdownHook最佳实践
1)关闭钩子本质上是一个线程(也称为Hook线程),对于一个JVM中注册的多个关闭钩子它们将会并发执行,所以JVM并不保证它们的执行顺序;由于是并发执行的,那么很可能因为代码不当导致出现竞态条件或死锁等问题,为了避免该问题,强烈建议在一个钩子中执行一系列操作。
2)Hook线程会延迟JVM的关闭时间,这就要求在编写钩子过程中必须要尽可能的减少Hook线程的执行时间,避免hook线程中出现耗时的计算、等待用户I/O等等操作。
3)关闭钩子执行过程中可能被强制打断,比如在操作系统关机时,操作系统会等待进程停止,等待超时,进程仍未停止,操作系统会强制的杀死该进程,在这类情况下,关闭钩子在执行过程中被强制中止。
4)在关闭钩子中,不能执行注册、移除钩子的操作,JVM将关闭钩子序列初始化完毕后,不允许再次添加或者移除已经存在的钩子,否则JVM抛出 IllegalStateException。
5)不能在钩子调用System.exit(),否则卡住JVM的关闭过程,但是可以调用Runtime.halt()。
6)Hook线程中同样会抛出异常,对于未捕捉的异常,线程的默认异常处理器处理该异常,不会影响其他hook线程以及JVM正常退出。
Hook线程关闭线程池
如果在Hook线程中,调用线程池的关闭方法,如果线程池存在未执行完的任务或阻塞任务,是不一定能正常关闭的线程池的,此时Hook线程正常执行完成,JVM退出,即使线程池里有未执行完成的任务,JVM仍然会退出。
此时一般要有等待机制, 等待一定的时间让线程池里的任务全部执行完成。同时提交到线程池里的任务要基于业务情况决定是否能容忍 JVM退出导致的任务中断。
线程池优雅关闭
shutdownnow
当我们调用线程池的shutdownNow时,线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行。如果线程刚刚在执行run,则正常执行完成后下一次poll队列上的任务时,getTask返回null, while循环直接退出,从而线程退出。
结论:执行完当前的任务,但是丢掉队列里的任务。
shutdown
线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池。等待线程池里当前线程的run执行完成,等待队列里的所有任务执行完成。
结论:
当我们调用线程池的shuwdown方法时,
如果线程正在执行线程池里的任务,即便任务处于阻塞状态,线程也不会被中断,而是继续执行。
如果线程池阻塞等待从队列里读取任务,则会被唤醒,但是会继续判断队列是否为空,如果不为空会继续从队列里读取任务,为空则线程退出。
所以当我们使用shutdownNow方法关闭线程池时,一定要对任务里进行异常捕获。(shutdown方法设置了线程的中断标记,如果线程处于阻塞中,会抛出中断异常,需要考虑中断异常是否会影响原有业务逻辑)
当我们使用shuwdown方法关闭线程池时,一定要确保任务里不会有永久阻塞等待的逻辑,否则线程池就关闭不了。
最后,一定要记得,shutdownNow和shuwdown调用完,线程池并不是立马就关闭了,要想等待线程池关闭,还需调用awaitTermination方法来阻塞等待。