关闭方式
正常关闭
- 最后一个普通线程(非守护线程)结束
- 调用了System.exit
- 发送SIGINT信号(相当于不带参数的kill命令)或者键入Ctrl-C
强制关闭
- 调用Runtime.halt
- 发送SIGKILL信号(kill -9 命令)
关闭钩子(Shutdown Hook)
钩子配置方法
通过下面的设置方法可看到,关闭钩子实际为线程
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
}));
触发调用Hook线程流程
正常关闭中,JVM首先调用所有已注册的关闭钩子。JVM并不能保证关闭钩子的调用顺序。在关闭应用程序线程时,如果有线程仍然在运行,那么这些线程接下来将与关闭进程并发进行。
当所有的关闭钩子都执行结束时,如果runFinalizersOnExit为true,那么JVM将运行终结器,然后再停止。JVM并不会停止或中断任何在关闭时仍然运行的应用程序线程。当JVM最终结束时,这些线程将被强行结束。
如果关闭钩子或终结器没有执行完成,那么正常关闭进程挂起并且JVM必须被强行关闭。
–《Java并发编程实战》
上述是书里面的介绍,下面跟着源码对照整个流程:
添加钩子
Runtime:
public void addShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
ApplicationShutdownHooks.add(hook);
}
ApplicationShutdownHooks:
会检查hook线程是否被启动,是否已经添加过
其中看到第一个判断条件是判断hooks是否为空,这是因为当ApplicationShutdownHooks开始启动钩子线程后,会把hooks置为空,也意味着开始启动钩子线程后,就再也无法添加钩子线程
/* Add a new shutdown hook. Checks the shutdown state and the hook itself,
* but does not do any security checks.
*/
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);
}
关闭钩子管理器的初始化
从上面代码可以看到,实际管理关闭钩子的是ApplicationShutdownHooks类,
可以看到ApplicationShutdownHooks其实是属于 用户/应用程序等级(user level) 的关闭钩子管理器(注释),该类初始化的时候,会把自己注册到虚拟机的关闭钩子队列中(Shutdown)(静态初始代码)
初始化:
/*
* Class to track and run user level shutdown hooks registered through
* <tt>{@link Runtime#addShutdownHook Runtime.addShutdownHook}</tt>.
*
* @see java.lang.Runtime#addShutdownHook
* @see java.lang.Runtime#removeShutdownHook
*/
class ApplicationShutdownHooks {
/* The set of registered hooks */
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;
}
}
...
}
关闭钩子的调用
因为ApplicationShutdownHooks自己也是作为一个关闭钩子注册到JVM的关闭钩子队列中,所以也是由Shutdown来触发,下面先看ApplicationShutdownHooks中的执行流程为:
- 获取ApplicationShutdownHooks类锁,然后然后把所有钩子线程转移出来,然后hooks置为空
- 启动所有钩子线程
- join所有钩子线程
第一个步骤,把hooks置为空后,在添加或者移除钩子的时候,会抛出异常,无法添加或者移除钩子,即当ApplicationShutdownHooks开始启动关闭钩子线程后,不能再添加、移除钩子
第二个步骤,启动线程并发执行,验证了书里面说的不能保证关闭钩子的调用顺序
执行:
/* Iterates over all application hooks creating a new thread for each
* to run in. Hooks are run concurrently and this method waits for
* them to finish.
*/
static void runHooks() {
Collection<Thread> threads;
synchronized(ApplicationShutdownHooks.class) {
threads = hooks.keySet();
hooks = null;
}
for (Thread hook : threads) {
hook.start();
}
for (Thread hook : threads) {
while (true) {
try {
hook.join();
break;
} catch (InterruptedException ignored) {
}
}
}
}
Shutdown解析
类功能
Shutdown是用于管理JVM关闭
/**
* Package-private utility class containing data structures and logic
* governing the virtual-machine shutdown sequence.
*
* @author Mark Reinhold
* @since 1.3
*/
class Shutdown {...}
添加钩子
再来看看添加钩子的方法,传递3个参数:slot,registerShutdownInProgress,hook
slot:存放关闭钩子的是一个固定大小的数组,并且每个槽位存放钩子是固定的,以及只能被初始化一次
registerShutdownInProgress:决定是否允许Shutdown开始执行关闭钩子线程后添加钩子,而ApplicationShutdownHooks在注册的时候传入的是false,因此如果Shutdown开始执行关闭钩子线程后,添加钩子线程会抛出IllegalStateException异常
/* Shutdown state */
private static final int RUNNING = 0;
private static final int HOOKS = 1;
private static final int FINALIZERS = 2;
private static int state = RUNNING;
// The system shutdown hooks are registered with a predefined slot.
// The list of shutdown hooks is as follows:
// (0) Console restore hook
// (1) Application hooks
// (2) DeleteOnExit hook
private static final int MAX_SYSTEM_HOOKS = 10;
private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS];
static void add(int slot, boolean registerShutdownInProgress, Runnable hook) {
synchronized (lock) {
if (hooks[slot] != null)
throw new InternalError("Shutdown hook at slot " + slot + " already registered");
if (!registerShutdownInProgress) {
if (state > RUNNING)
throw new IllegalStateException("Shutdown in progress");
} else {
if (state > HOOKS || (state == HOOKS && slot <= currentRunningHook))
throw new IllegalStateException("Shutdown in progress");
}
hooks[slot] = hook;
}
}
执行过程
最核心的执行方法为,流程是
- 获取锁,判断当前状态是否处于HOOKS状态(正在执行关闭钩子),如果是直接退出
- 执行所有关闭钩子任务,注意这里不是启动线程,而只是单纯的调用方法
- 更新当前状态到FINALIZERS
- 判断是否需要执行finalize方法,如果需要则执行 P.S. runFinalizersOnExit参数可通过set方法设置
正如前面提到,JVM正常关闭的几个触发点,都会触发调用该方法
p.s. 关于runFinalizersOnExit参数,由目前代码来看,该参数已被弃用
/* Run all registered shutdown hooks
*/
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;
}
}
}
}
private static void sequence() {
synchronized (lock) {
/* Guard against the possibility of a daemon thread invoking exit
* after DestroyJavaVM initiates the shutdown sequence
*/
if (state != HOOKS) return;
}
runHooks();
boolean rfoe;
synchronized (lock) {
state = FINALIZERS;
rfoe = runFinalizersOnExit;
}
if (rfoe) runAllFinalizers();
}
钩子触发
当所有普通线程都结束的时候,会触发该shutdown方法(被DestroyJavaVM调用),执行关闭钩子,而且该方法不会关掉JVM
p.s. 唯一的普通线程出现未捕获异常而退出,其实也属于这种情况,因此也会调用shutdown方法
/* 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();
}
}
另外就是调用了System.exit,以及接收到SIGINT,SIGTERM信号时的处理,这两种触发底层实际都为调用Shutdown.exit方法,其中执行完finalize方法以及关闭钩子后,才会执行halt方法关闭JVM,验证了书中所说,终结器和关闭钩子未完成的情况下, JVM不会关闭退出。
可以看到,只有在传入的status为0的情况下(正常退出),才会执行runAllFinalizers方法(终结器)
/* Invoked by Runtime.exit, which does all the security checks.
* Also invoked by handlers for system-provided termination events,
* which should pass a nonzero status code.
*/
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();
halt(status);
}
}
System.exit的调用:
public final class System {
...
public void exit(int status) {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkExit(status);
}
Shutdown.exit(status);
}
...
}
处理SIGINT,SIGTERM信号的调用,具体来说,下面的代码不是触发调用的地方,根据注释,Terminator属于负责 安装和卸载 处理平台终端信息处理器,并由System类在线程初始化后调用(Initialize the system class. Called after thread initialization.)。
从方法也可以看到,提供了setup和teardown两个方法,其中,setup中可以看到,针对INT和TERM信号处理是调用Shutdown.exit,后面流程大概就是接受到INT和TERM信号,然后调用相应的处理器进行处理(没有看到相关代码)
/**
* Package-private utility class for setting up and tearing down
* platform-specific support for termination-triggered shutdowns.
*
* @author Mark Reinhold
* @since 1.3
*/
class Terminator {
private static SignalHandler handler = null;
/* Invocations of setup and teardown are already synchronized
* on the shutdown lock, so no further synchronization is needed here
*/
static void setup() {
if (handler != null) return;
SignalHandler sh = new SignalHandler() {
public void handle(Signal sig) {
Shutdown.exit(sig.getNumber() + 0200);
}
};
handler = sh;
// When -Xrs is specified the user is responsible for
// ensuring that shutdown hooks are run by calling
// System.exit()
try {
Signal.handle(new Signal("INT"), sh);
} catch (IllegalArgumentException e) {
}
try {
Signal.handle(new Signal("TERM"), sh);
} catch (IllegalArgumentException e) {
}
}
static void teardown() {
/* The current sun.misc.Signal class does not support
* the cancellation of handlers
*/
}
}
Signal中的触发方法
private static void dispatch(int var0) {
final Signal var1 = (Signal)signals.get(var0);
final SignalHandler var2 = (SignalHandler)handlers.get(var1);
Runnable var3 = new Runnable() {
public void run() {
var2.handle(var1);
}
};
if (var2 != null) {
(new Thread(var3, var1 + " handler")).start();
}
}
强制关闭
在开始时候有提到的强制关闭的几种方法,其中Runtime.halt,看源码可知其实际调用的为Shutdown.halt方法,即不调用关闭钩子,直接关闭JVM
static void halt(int status) {
synchronized (haltLock) {
halt0(status);
}
}
SIGKILL信号的处理,没看到哪里有注册相关处理器的代码
总结
最后总结下jvm关闭的相关流程
JVM关闭流程
- ApplicationShutdownHooks初始化的时候,把自身runHooks方法作为钩子任务注册到Shutdown中
- 用户调用Runtime.getRuntime().addShutdownHook,注册钩子线程到ApplicationShutdownHooks中
- 当 System.exit or Runtime.exit or 接收到SIGINT,SIGTERM信号,将触发Shutdown.exit方法;或者 所有普通线程都结束的时候 触发 Shutdown.shutdown方法;这两个方法,都会让Shutdown执行所有注册到自身的钩子任务(非线程)
- Shutdown执行钩子任务的时候,会执行ApplicationShutdownHooks的钩子任务runHooks方法, 这时会启 动所有注册到ApplicationShutdownHooks的钩子线程
- ApplicationShutdownHooks.runHooks主线程会等待所有钩子线程执行完成才退出
- Shutdown执行完所有钩子任务后,如果是Shutdown.exit方法,最后会执行halt方法关闭JVM
- 如果是调用Runtime.halt or 接收到SIGKILL信号,则直接关闭JVM
自问自答
正常关闭的几个方法中,所有普通线程都结束的情况下,为什么最后不需要调用halt方法?
虽然源码中没有说明,但猜测是,因为其他情况下,普通线程还在执行中,所以需要通过halt方法把其他线程一并杀掉
来源:CSDN
作者:ZhongWenhui_1995
链接:https://blog.csdn.net/name_z/article/details/104179693