JVM关闭的方式

正常退出

全部非Daemon线程执行结束

所有非守护线程执行结束后,JVM会自动退出。此时如果有Daemon线程,仍然会自动退出。

  1. public class Exits {
  2. public static void main(String[] args) {
  3. Thread t = new Thread(new Runnable() {
  4. @SneakyThrows
  5. @Override
  6. public void run() {
  7. Thread.sleep(2000);
  8. System.out.println(Thread.currentThread().getName());
  9. }
  10. });
  11. t.setName("foolBoy");
  12. t.setDaemon(false);
  13. t.start();
  14. Thread daemon = new Thread(new Runnable() {
  15. @SneakyThrows
  16. @Override
  17. public void run() {
  18. Thread.sleep(5000);
  19. System.out.println(Thread.currentThread().getName());
  20. }
  21. });
  22. daemon.setName("daemon");
  23. daemon.setDaemon(true);
  24. daemon.start();
  25. }
  26. }

System.exit(0)

显示调用exit方法,JVM会退出

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关闭时,执行一个线程。

  1. Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
  2. @Override
  3. public void run() {
  4. System.out.println("Clean Resource....");
  5. }
  6. }));

基于信号的进程通知机制

信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。通俗来讲,信号就是进程间的一种异步通信机制。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方法来阻塞等待。