前言
Java程序启动从main函数开始启动,是程序入口和主线程,但程序会在什么时候结束?为什么有的Java程序在启动后很快就结束了,比如HelloWorld程序,有的程序却能一直在运行,比如Tomcat启动后就一直保持进程不关闭。使用kill -15 PID
关闭JVM进程究竟有没有问题?为了搞清楚这些问题,本文来详细分析一下JVM退出的机制。
JVM的退出介绍
JVM的退出可以分正常退出、异常退出和强制退出,每种退出方法的不同会产生不过的情况,汇总如下:
Linux操作系统关闭不完全支持优雅退出,原因是Linux关闭时先会向进程发送SIGTERM信号,等待一段时间进程还没退出时就会强制关闭进程,所以Linux只会给一定时间让进程关闭退出。
JVM可通过以下几种方式正常退出:
- 最后一个非守护线程结束。
- JVM被中断(通过ctrl + c或发送SIGINT信号)。
- JVM被终止(通过发送SIGTERM信号,即kill -15 PID或kill PID)。
- 某个线程调用
System.exit()
或Runtime.exit()
。
当System.exit(int)
被调用时,会通过security manager进行检查,是否允许以给定的状态退出,当允许时会调用Shutdown.exit()
当向JVM发送中断信号(SIGINT)或终止信号(SIGTERM),不经过security manager检查,直接调用Shutdown.exit()
Shutdown
类的exit()
会运行Shutdown Hook,通过一个锁防止这些Hook执行两次,在最后会调用halt(int)
真正的去关闭JVM。
为什么SIGKILL
(kill -9
)无法实现应用的优雅关闭?
SIGKILL(使用 kill -9 命令发送)无法实现应用的优雅关闭,因为它是一种无条件的终止信号,会立即终止目标进程,而不给进程执行任何清理或收尾工作的机会。这包括关闭文件、释放资源、保存状态等。简而言之,SIGKILL不会让进程有机会进行任何“优雅”的关闭操作。
相反,常规的进程终止信号 SIGTERM 允许进程执行清理工作。当你发送 SIGTERM 信号时,进程会收到这个信号并可以自行决定如何处理它,比如关闭文件、释放资源、保存状态等,然后正常退出。这种方式更为优雅,因为它给了应用程序执行关闭过程的机会。
ShutdownHook
在具体分析JVM的每种退出方式之前先来了解一下与退出机制息息相关的概念:ShutdownHook(关闭钩子)。
ShutdownHook(关闭钩子)是一个已经初始化但尚未启动的线程。当虚拟机开始其关闭步骤时,它会以某种未指定的顺序启动所有已注册的关闭钩子,并让它们并发运行。当所有钩子都完成后,如果启用了退出时的清理(finalization-on-exit),那么它会运行所有尚未调用的终结器。最后,虚拟机将停止。关闭钩子应该尽快完成它们的工作。当一个程序调用exit时,期望是虚拟机会迅速关闭并退出。当虚拟机因外部因素(如用户中断或系统事件)而终止时,关闭钩子提供了一个机会来执行一些清理工作或保存状态,但同样应该尽快完成。
以下是一个简单的Shutdown Hook栗子:
public class SimpleShutdownHookTest {public static void main(String[] args) {MyHook myHook1 = new MyHook("hook-1");MyHook myHook2 = new MyHook("hook-2");Runtime.getRuntime().addShutdownHook(myHook1);Runtime.getRuntime().addShutdownHook(myHook2);System.exit(0); //系统退出,会启动Shutdown Hook线程}static class MyHook extends Thread {public MyHook(String name) {super.setName(name);}public void run() {try {System.out.println("do shutdown " + Thread.currentThread().getName());Thread.sleep(10 * 1000);} catch (InterruptedException e) {e.printStackTrace();}}}}
ShutdownHook是实现程序优雅退出的关键,提供了一种方让开发者回收资源、关闭句柄、结束任务等工作。
Java程序优雅退出触发的场景和处理ShutdownHook的过程归纳起来如下图:
System.exit(int)处理过程
Java虚拟机规范有描述到JVM的退出:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.7
5.7. Java Virtual Machine ExitThe Java Virtual Machine exits when some thread invokes the exit method of class Runtime or class System, or the halt method of class Runtime, and the exit or halt operation is permitted by the security manager.In addition, the JNI (Java Native Interface) Specification describes termination of the Java Virtual Machine when the JNI Invocation API is used to load and unload the Java Virtual Machine.
这段话翻译为中文:
JVM在某个线程调用Runtime
类或System
类的exit
方法,或者调用Runtime
类的halt
方法,并且这些exit
方法或halt
方法操作被安全管理器允许时,JVM退出。
此外,JNI(Java Native Interface)规范描述了在使用JNI调用API加载和卸载Java虚拟机时,Java虚拟机的终止情况。
后半段JNI这段话的意思是使用JNI调用本地库的方法时,这个方法里面包含有加载或退出虚拟机的逻辑。
总结一下就是调用以下三个方法之一会使JVM退出:
- System.exit(int)
- Runtime.exit(int)
- Runtime.halt(int)
其中System.exit(int)*调用的是Runtime.exit(int),这两个是同样的效果
public final class System {//省略代码...public static void exit(int status) {Runtime.getRuntime().exit(status);}
}
Runtime.exit(int):
public class Runtime {/*** Terminates the currently running Java virtual machine by initiating its* shutdown sequence. This method never returns normally. The argument* serves as a status code; by convention, a nonzero status code indicates* abnormal termination.** <p> The virtual machine's shutdown sequence consists of two phases. In* the first phase all registered {@link #addShutdownHook shutdown hooks},* if any, are started in some unspecified order and allowed to run* concurrently until they finish. In the second phase all uninvoked* finalizers are run if {@link #runFinalizersOnExit finalization-on-exit}* has been enabled. Once this is done the virtual machine {@link #halt* halts}.** <p> If this method is invoked after the virtual machine has begun its* shutdown sequence then if shutdown hooks are being run this method will* block indefinitely. If shutdown hooks have already been run and on-exit* finalization has been enabled then this method halts the virtual machine* with the given status code if the status is nonzero; otherwise, it* blocks indefinitely.** <p> The <tt>{@link System#exit(int) System.exit}</tt> method is the* conventional and convenient means of invoking this method. <p>** @param status* Termination status. By convention, a nonzero status code* indicates abnormal termination.** @throws SecurityException* If a security manager is present and its <tt>{@link* SecurityManager#checkExit checkExit}</tt> method does not permit* exiting with the specified status** @see java.lang.SecurityException* @see java.lang.SecurityManager#checkExit(int)* @see #addShutdownHook* @see #removeShutdownHook* @see #runFinalizersOnExit* @see #halt(int)*/public void exit(int status) {SecurityManager security = System.getSecurityManager();if (security != null) {security.checkExit(status);}Shutdown.exit(status);}
}
上面exit
方法的源码上的注释:
- 传入参数status为0时,是正常退出
- 传入参数status不0时,为异常退出
JVM的关闭步骤包含两个步骤:
- 运行已经注册的ShutdownHook,它们被无序的执行直到完成。
- 如果用
setRunFinalizersOnExit
设置为true,在关闭之前将会继续调用所有未被调用的 finalizers 方法。
class Shutdown {static void exit(int status) {boolean runMoreFinalizers = false;synchronized (lock) {if (status != 0) runFinalizersOnExit = false;switch (state) {case RUNNING: /* Initiate shutdown */state = HOOKS;break;case HOOKS: /* Stall and halt */break;case FINALIZERS:if (status != 0) {/* Halt immediately on nonzero status */halt(status);} else {/* Compatibility with old behavior:* Run more finalizers and then halt*/runMoreFinalizers = runFinalizersOnExit;}break;}}if (runMoreFinalizers) {runAllFinalizers();halt(status);}synchronized (Shutdown.class) {/* Synchronize on the class object, causing any other thread* that attempts to initiate shutdown to stall indefinitely*///开始序列sequence();//强制终止当前正在运行的Java虚拟机。//这个方法接受一个整数参数作为退出状态码,表示程序的退出状态。halt(status);}}private static void sequence() {synchronized (lock) {DestroyJavaVM initiates the shutdown sequence//防在止DestroyJavaVM开始关闭序列步骤后,另一个线程调用exit造成两次运行if (state != HOOKS) return;}runHooks();boolean rfoe;synchronized (lock) {state = FINALIZERS;rfoe = runFinalizersOnExit;}if (rfoe) runAllFinalizers();}private static void runHooks() {for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {try {Runnable hook;synchronized (lock) {// acquire the lock to make sure the hook registered during// shutdown is visible here.currentRunningHook = i;hook = hooks[i];}if (hook != null) hook.run();} catch(Throwable t) {if (t instanceof ThreadDeath) {ThreadDeath td = (ThreadDeath)t;throw td;}}}}
} class ApplicationShutdownHooks {private static IdentityHashMap<Thread, Thread> hooks;static {try {//Shutdown.add(1 /* shutdown hook invocation order */,false /* not registered if shutdown in progress */,new Runnable() {public void run() {runHooks();}});hooks = new IdentityHashMap<>();} catch (IllegalStateException e) {// application shutdown hooks cannot be added if// shutdown is in progress.hooks = null;}}static synchronized void add(Thread hook) {if(hooks == null)throw new IllegalStateException("Shutdown in progress");if (hook.isAlive())throw new IllegalArgumentException("Hook already running");//防止同一个钩子多次注册if (hooks.containsKey(hook))throw new IllegalArgumentException("Hook previously registered");//增加钩子hooks.put(hook, hook);}static void runHooks() {Collection<Thread> threads;synchronized(ApplicationShutdownHooks.class) {threads = hooks.keySet();hooks = null;}for (Thread hook : threads) {hook.start();}for (Thread hook : threads) {try {hook.join(); //等待钩子线程执行完成} catch (InterruptedException x) { }}}}
System.exit(int)对于关闭钩子的处理时序如下图:
非守护线程运行完成退出JVM
Shutdown.shutdown()
的JavaDoc提到当最后一个非守护线程完成,本地DestroyJavaVM程序会调用Shutdown.shutdown()
;与Shutdown.exit(int)
不同的是Shutdown.shutdown()
不会真正去终止JVM,而是由DestroyJavaVM程序终止。
/* Invoked by the JNI DestroyJavaVM procedure when the last non-daemon* thread has finished. Unlike the exit method, this method does not* actually halt the VM.*/static void shutdown() {synchronized (lock) {switch (state) {case RUNNING: /* Initiate shutdown */state = HOOKS;break;case HOOKS: /* Stall and then return */case FINALIZERS:break;}}synchronized (Shutdown.class) {sequence(); //开始序列}}
来测试一下这个说法,把调试断点放在Shutdown.shutdown()
第一行,运行一个最简单的main函数,在main函数这个唯一的非守护线程结束后,断点会运行到Shutdown.shutdown()
,验证了这个说法。
类似的说法在线程类Thread
的setDaemon(..)
方法JavaDoc也有提到:当JVM只有守护线程时,JVM会退出。
public class Thread implements Runnable {/*** Marks this thread as either a {@linkplain #isDaemon daemon} thread* or a user thread. The Java Virtual Machine exits when the only* threads running are all daemon threads.*/public final void setDaemon(boolean on) {checkAccess();if (isAlive()) {throw new IllegalThreadStateException();}daemon = on;}}
最后一个非守护线程结束后JVM关闭的流程图:
下面介绍一下用户线程和守护线程
Java线程分为两类:
- 1、用户线程(非守护线程)
- 2、守护线程(后台线程)
守护线程是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程都是守护线程。与之对应的是用户线程,用户线程可以理解为是系统的工作线程,它会完成这个程序需要完成的业务操作。如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出了,所以结束所有用户线程的运行,就可以使JVM关闭限出。
可以使用jstack -l PID
查看线程有没有daemon修饰判断是用户线程还是守护线程。
来盘点一下让用户线程退出的方法:
1、调用线程的stop()方法(已废弃)
直接退出线程,因为太暴力会产生不可知的结果该方法已废弃。
2、调用线程的interrupt()方法
需要注意的是线程的interrupt()方法不会直接停止线程的运行,需要在interrupt方法后出现的情况在程序自行通过编码结束。当调用线程的interrupt()方法根据以下两种情况出现不同结果:
- 2.1 当使用
interrupt()
方法去打断处于阻塞状态的线程时,会抛出InterruptedException
异常,而不会更新打断标记,因此,虽然被打断,但是打断标记依然为false。
Thread#isInterrupted()方法可返回打断标记
线程阻塞的情况有以下这些:
* @see java.lang.Object#wait()* @see java.lang.Object#wait(long)* @see java.lang.Object#wait(long, int)* @see java.lang.Thread#sleep(long)* @see java.lang.Thread#sleep(long)* @see java.util.concurrent.locks.Condition.await
- 2.2 当使用interrupt()方法去打断正在运行线程时,被打断的线程会继续运行,但是该线程的打断标记会更新,更新为true,因此可以根据打断标记来作为判断条件使得线程停止。线程是否打断的方法为isInterrupted()
须注意的是调用线程的interrupt()方法并不会停止和关闭线程,程序自行根据打断标记或InterruptedException异常自行结束线程的运行
下面是一个interrupt非守护线程后通过判断线程中断状态结束程序运行的例子:
public class ThreadExitTest {public static void main(String[] args) {Thread t = new Thread(new Runnable() {@Overridepublic void run() {try {for (int i = 0; i < 100; i++) {System.out.println("Task " + i);if (Thread.currentThread().isInterrupted()) {//如果线程状态为中断,退出循环System.out.println("Thread interrupted! Exiting loop.");return;}Thread.sleep(1000); // 模拟执行任务的耗时}} catch (InterruptedException e) {System.out.println("Thread interrupted! Exiting thread.");// 设置线程的中断状态,以确保线程可以正确退出// 如果捕获异常后其它事情可做,也可以直接在此处returnThread.currentThread().interrupt();}}});t.setDaemon(false);t.start();// 让主线程等待一段时间后中断子线程try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}t.interrupt();}}
再来看看最简单的HelloWorld程序:
public class HelloWorld {public static void main(String[] args) {System.out.println("hello world!");}
}
这是main函数只有一行打印输出控制台的代码,根据上面的理论,就很容易解析为什么HelloWorld程序在打印完hello world!后进程就会退出。
首先当HelloWorld程序启动后,JVM只有一个用户线程main
线程,当执行打印的代码后,main线程的任务已经运行完毕,紧接下来的是main线程的结束。当main线程结束后,JVM已经没有用户线程,JVM随之退出。
下面再来看看另一个栗子,起一个子线程,子线程睡眠60秒。
import java.util.concurrent.TimeUnit;public class JvmExistWhenNonDaemonThreadRunning {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(new Runnable() {@Overridepublic void run() {try {Thread.sleep(TimeUnit.SECONDS.toMillis(60));} catch (InterruptedException e) {e.printStackTrace();}}});t.setDaemon(false);t.start();System.out.println("non daemon thread has started...");}
}
启动后用以下命令来查询JVM的非守护线程
$ jstack -l 46596 | awk '/tid/ && !/daemon/ {print $0; for(i=1;i<=10;i++) {getline; print}}'
咦!怎么跟上面讲的不一样,除了子线程"Thread-0"外,还有"DestroyJavaVM"、“VM Thread”、“GC task thread#0 (ParallelGC)”、"VM Periodic Task Thread"等非守护线程。当子线程"Thread-0"运行完毕后,还有这几个非守护线程,这样是不是导致JVM没法退出?
这里引申出另外一个知识点,在main函数结束后,JVM会自动启动一个DestroyJavaVM线程,该线程会等待所用户线程结束后退出(即只剩下daemon 线程、DestroyJavaVM线程自己、VM Thread、VM Periodic Task Thread、GC线程等系统非守护线程,整个虚拟机就退出,此时守护线程被终止)。由此可知这些系统非守护线程并不会影响所有非守护线程结束后JVM的关闭。
系统非守护线程 | 说明 |
---|---|
DestroyJavaVM | 在JVM中所有其他非守护线程全部结束后负责销毁虚拟机。DestroyJavaVM线程在JVM的生命周期中扮演着非常重要的角色,确保资源得到正确的清理和释放。 |
VM Thread | 这个线程等待在 JVM 到达安全点进行操作时出现,该线程执行的操作包括“stop the world”的垃圾收集、线程堆栈dump、线程挂起和偏向锁。 |
VM Periodic Task Thread | VM Periodic Task Thread是JVM中的一个特殊线程,主要负责执行一些周期性的后台任务,包括垃圾回收、性能监控、统计信息收集等。 |
GC task thread#0 (ParallelGC) | 并行垃圾回收器, 使用java启动参数-XX:+UseParNewGC时使用这个垃圾回收器。 |
思考一下问题,上面那个子线程睡眠的例子在运行时正常关闭JVM会出现问题吗?
正常关闭的所包含场景可以回看本文第一张配图
答案是会出现问题的。因为JVM退出的所有的清理和关闭钩子都没有对这个睡眠线程作处理,这个线程其实没有得到优雅的退出处理的,最后会让JVM强制关闭退出,线程由此不可控的退出。这种粗暴的退出线程处理在一些对数据保存的场景是不可接受的,比如先将数据保存到数据库,然后更新缓存这两个步骤,如果在第一步保存数据到数据库完成后就线程就被强制退出了,导致数据库和缓存的不一致。
非守护线程的优雅关闭
JVM所有优雅退出的情况都会在退出的时候调用关闭钩子,所以可以用上面介绍到的关闭钩子去实现,以下是一个通过关闭钩子中断任务线程的栗子。每次开始下一次任务时任务线程会根据线程状态是否中断来进行继续下一个任务或结束线程的运行,当JVM退出运行关闭钩子时,中断线程,任务线程的状态设置为中断,任务线程结束运行。
public class JvmThreadElegantExit {public static void main(String[] args) {//任务线程Thread taskThread = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 1000; i++) {try {if (Thread.currentThread().isInterrupted()) {System.out.println("task thread is interrupt, exit now...");break;}System.out.println("task " + i + "has done..."); //模拟一次任务处理Thread.sleep(1000);} catch (InterruptedException e) {Thread.currentThread().interrupt(); //将线程状态设为中断}}}});taskThread.start();//关闭钩子Thread shutdownHook = new Thread(new Runnable() {@Overridepublic void run() {//中断任务线程taskThread.interrupt();}});Runtime.getRuntime().addShutdownHook(shutdownHook);//让主线程等待一段时间后关闭JVMtry {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.exit(0); //这行也可去掉,改为用kill -15 PID退出JVM}
}
线程池ThreadPoolExecutor的关闭
线程池的工作线程默认为非守护线程,其中的核心线程(corePoolSize)空闲时默认不会关闭退出,提交一个任务创建工作线程后不对线程池作操作的话,工作线程会一直保持存活,我们的预期是工作完成后JVM自动退出的,但实际情况是和预期不一致。
这个是一个线程池工作完成后JVM进程一直存活不会退出的栗子:
public class ThreadPoolExecutorKeepAliveExample {public static void main(String[] args) throws InterruptedException {ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS, new LinkedBlockingDeque());threadPoolExecutor.execute(new Runnable() {@Overridepublic void run() {try {for (int i = 0; i < 10; i++) {System.out.println("sub job " + i + " had done");Thread.sleep(1000);}} catch (InterruptedException e) {e.printStackTrace();}System.out.println("all job had done...");}}););}}
上面是一个线程池里循环执行N个子任务的栗子,在程序启动后对JVM发送SIGTERM信号可以使工作线程关闭,但在Shutdown Hook线程运行完毕后就强制关闭JVM,没有给工作线程优雅关闭的时机,工作线程在工作中时被强制关闭可能导致任务执行不完整。
如果在JVM退出的时候优雅的关闭?
可以在Shutdown Hook里调用线程池的shutdown()
方法并使用awaitTermination(..)
等待工作线程完成工作。
public class ThreadPoolExecutorElegantShutdown {public static void main(String[] args) throws InterruptedException {ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(50));threadPoolExecutor.execute(new Runnable() {@Overridepublic void run() {System.out.println("job begin...");try {for (int i = 0; i < 10; i++) {System.out.println("job processing " + ((i + 1) * 10) + "%");Thread.sleep(2000);}} catch (InterruptedException e) {e.printStackTrace();}System.out.println("job had done...");}});//关闭钩子线程Thread hookThread = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("shutdown hook begin...");//关闭线程池threadPoolExecutor.shutdown();try {//等待30秒使工作线程完成threadPoolExecutor.awaitTermination(30, TimeUnit.SECONDS);System.out.println("shutdown hook end...");} catch (InterruptedException e) {e.printStackTrace();}}});//注册关闭钩子Runtime.getRuntime().addShutdownHook(hookThread);}
}
JVM启动后,可以用以下命令发送SIGTERM信号:
jps -l | grep ThreadPoolExecutorElegantShutdown | awk '{print $1}' | xargs kill
程序控制台的输出:
job begin...
job processing 10%
job processing 20%
job processing 30%
shutdown hook begin...
job processing 40%
job processing 50%
job processing 60%
job processing 70%
job processing 80%
job processing 90%
job processing 100%
job had done...
shutdown hook end...
可以看出JVM进程在kill命令后工作线程的任务还是继续工作直至完成。
这里用threadPoolExecutor.awaitTermination(30, TimeUnit.SECONDS)
使Shutdown Hook线程等待30秒,在上面Systen.exit(int)
方法介绍到DestroyVM线程在Shutdown Hook线程启动后会调用Shutdown Hook线程的join()
方法等待Shutdown Hook线程完成,如果这里只等待5秒而线程池工作线程还没有结束的话,Shutdown Hook线程结束后DestroyVM就会关闭JVM,也会导致线程池工作线程中断,换句话来说就是只会等待timeout时间让工作线程完成工作。要解决在等待timeout时间后工作线程还没结束的问题,可以把等待的timeout时间设置更长一点,如果线程池工作线程结束的快,不会多浪费时间等待在设定的timeout,在线程池所有工作线程完成后线程池状态变为TERMINATED便会唤醒等待。
计划线程池ScheduledThreadPoolExecutor的关闭
计划线程池ScheduledThreadPoolExecutor和ThreadPoolExecutor一样使用shutdown()
关闭,但区别就是计划线程池可以根据业务需要设置参数决定shutdown()
后是否要继续运行任务:
- executeExistingDelayedTasksAfterShutdown:是否在shutdown后继续运行延迟任务
- continueExistingPeriodicTasksAfterShutdown:是否在shutdown后继续运行周期性任务
参考:
https://juejin.cn/post/7274046488752586811?from=search-suggest
https://stackoverflow.com/questions/32315589/what-happens-when-the-jvm-is-terminated