JVM钩子

JVM进程在接收到kill -15信号通知的时候,是可以做一些清理动作的,比如:删除临时文件等。当然了,开发者也可以基于该信号自定义做一些额外的操作,比如:让tomcat容器停止,让dubbo服务下线,清理数据等。这种自定义JVM清理动作的方式,是通过JDK中提供的ShutdownHook实现的。JDK提供了Java.Runtime.addShutdownHook(Thread hook) 方法,可以注册一个在JVM关闭时需要执行的钩子方法。

kill命令原理请见博文:kill -9的原理

JVM钩子的作用

JVM钩子方法的执行时机是在所有非守护线程都执行完成之后,在JVM进程退出之前,利用钩子线程去执行一些清理工作,如:释放资源,文件清理等等。

我们都知道,在Linux中,Java应用是作为一个独立进程运行的,Java程序的终止就是基于JVM进程的关闭实现的,JVM进程关闭方式分为3种:

  • 正常关闭:当最后一个非守护线程运行结束 或者 调用了 System.exit( ) 或者 通过其他特定平台的方法关闭 或者 执行 kill [-15](接收到SIGINT(2)、SIGTERM(15)信号等);
  • 异常关闭:程序运行中遇到RuntimeException异常,或者操作系统程序出现异常导致JVM退出等;
  • 强制关闭:通过调用Runtime.halt( )方法 或者 在操作系统中强制 kill -9(接收到SIGKILL(9)信号)

Linux常见的信号有:

  • kill -2 PID —— 正常中断进程(作用等同于:Ctrl + C )。程序在结束之前,能够保存相关数据,然后再退出。
  • kill -9 PID —— 强制杀死一个进程。
  • kill [-15] PID —— 正常方式关闭(终止)进程。关闭进程时应先考虑使用 kill -15 ,以便于其能够预先清理临时文件和释放资源。

PS:kill -9 作为最后手段,应对那些失控的进程。

JVM钩子的使用场景

JVM正常关闭 – 钩子生效

如果有注册钩子,那么当JVM进程中的所有非守护线程执行完任务之后,JVM退出之前会执行钩子里面的任务。

Demo1:

1
2
3
4
5
6
public static void main(String[] args) {
System.out.println("Hello World!!!");
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Hook Method...");
}));
}

输出:

1
2
Hello World!!! 
Hook Method...

从上述代码可以看出,我们一般定义的钩子任务都是被封装成一个线程任务,也就是说每个钩子都分别是在一个不同的线程中并发执行的。

源码分析

IDEA中debug上述Demo,发现在JVM进程中当所有的非守护线程结束时,JVM底层的C源码会调用JNI创建一个名为:**DestroyJavaVM线程 **用于执行注册的钩子任务。

DestroyJavaVM线程定义

java.c 源码文件中找到DestroyJavaVM线程的定义。如下图:

* Wait for all non-daemon threads to end, then destroy the VM.

* This will actually create a trivial new Java waiter thread named “DestroyJavaVM”, but this will be seen as a different thread from the one that executed main, even though they are the same C thread. This allows mainThread.join( ) and mainThread.isAlive() to work as expected.

译文:等待所有非守护线程结束,然后销毁VM。这实际上会创建一个名为“DestroyJavaVM”的Java等待线程,但这将被视为一个不同于执行main的线程,尽管它们是同一个C线程。这允许mainThread.join( )和mainThread.isAlive( )按预期工作。

从上述debug的截图来看,这个DestroyJavaVM线程主要就是调用了Shutdown.shutdown()方法来执行注册的钩子。钩子任务的整个执行链路如下图所示:

Shutdown.shutdwon( )源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 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.
* 译文:当最后一个非守护线程执行完成时,由JNI DestroyJavaVM过程调用。与exit方法不同的是,此方法实际上不会停止虚拟机。
*/
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.sequence( )源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* The actual shutdown sequence is defined here.
*
* If it weren't for runFinalizersOnExit, this would be simple -- we'd just
* run the hooks and then halt. Instead we need to keep track of whether
* we're running hooks or finalizers. In the latter case a finalizer could
* invoke exit(1) to cause immediate termination, while in the former case
* any further invocations of exit(n), for any n, simply stall. Note that
* if on-exit finalizers are enabled they're run iff the shutdown is
* initiated by an exit(0); they're never run on exit(n) for n != 0 or in
* response to SIGINT, SIGTERM, etc.
* 译文: 实际的关闭顺序在这里定义。
* 如果没有runFinalizersOnExit,这将是简单的——我们只需运行钩子,然后停止。相反,我们需要跟踪运行的是钩子还是终结器。
* 在后一种情况下,终结器可以调用exit(1)来导致立即终止,而在前一种情况下,任何对exit(n)的进一步调用,对于任何n,
* 都只是暂停。注意,如果启用了on-exit终结器,那么如果关闭由一个exit(0)启动,它们就会运行;它们永远不会在退出(n)时运行,
* 因为n != 0或响应SIGINT、SIGTERM等。
*/
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.runHooks( )源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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;
}
}

/* 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;
}
}
}
}

ApplicationShutdownHooks.run( )源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ApplicationShutdownHooks {
/* The set of registered hooks */
// IdentifyHashMap的解释: https://blog.csdn.net/f641385712/article/details/81880711
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.runHooks() – 执行钩子任务的核心方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* 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 {
// 这行代码是钩子任务的灵魂,阻塞当前线程直到所有的钩子任务线程执行完毕。
// 同时需要注意正是因为该行代码的存在导致在钩子任务中请勿执行耗时长的任务,否则会导致JVM进程长时间无法退出。
hook.join();
break;
} catch (InterruptedException ignored) {
}
}
}
}

开发者在注入钩子时使用的addShutdownHook()方法,就是进行注册钩子。

Runtime.addShutdownHook( )方法源码:

1
2
3
4
5
6
7
public void addShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
ApplicationShutdownHooks.add(hook);
}

ApplicationShutdownHooks.add()方法源码:

1
2
3
4
5
6
7
8
9
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);
}

至此,JVM进程正常结束时,注册钩子任务的执行流程源码分析就结束了,从开发者代码注入钩子到执行钩子的触发时机,每一步的对应源码分析都很仔细,后续再熟悉熟悉整个流程。

Demo2:

1
2
3
4
5
6
7
8
public static void main(String[] args) throws InterruptedException {
System.out.println("Hello World!!!");
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Hook Method...");
}));
System.exit(0);
TimeUnit.SECONDS.sleep(10);
}

输出:

1
2
Hello World!!!
Hook Method...

System.exit ( )方法源码分析

1
2
3
public static void exit(int status) {
Runtime.getRuntime().exit(status);
}

Runtime.exit( )方法

1
2
3
4
5
6
7
public void exit(int status) {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkExit(status);
}
Shutdown.exit(status);
}

Shutdown.exit( )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* 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); // 关闭JVM
} 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(0)方法结束JVM进程时,还是会执行钩子任务的。

JVM异常关闭 – 钩子生效

JVM进程在发生异常情况导致退出时,钩子还是会生效。

Demo:

1
2
3
4
5
6
7
public static void main(String[] args) {
System.out.println("Hello World!!!");
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Hook Method...");
}));
int i = 1 / 0;
}

输出:

1
2
3
4
Hello World!!!
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.maple.system.Demo_System_Exit.main(Demo_System_Exit.java:17)
Hook Method...

从debug可以看出,钩子的执行和JVM进程正常结束时是一样的:

JVM强制关闭 – 钩子不生效

JVM进程如果是被执行了kill -9Runtime.halt()方法等强制关闭的场景下,钩子是不生效的。

Demo:

1
2
3
4
5
6
7
8
public static void main(String[] args) throws InterruptedException {
System.out.println("Hello World!!!");
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Hook Method...");
}));
Runtime.getRuntime().halt(0);
TimeUnit.SECONDS.sleep(10);
}

输出:

1
Hello World!!!

Runtime.halt( )方法分析

1
2
3
4
5
6
7
public void halt(int status) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkExit(status);
}
Shutdown.halt(status);
}

Shutdown.halt( )

1
2
3
4
5
6
7
8
9
10
11
/* The halt method is synchronized on the halt lock
* to avoid corruption of the delete-on-shutdown file list.
* It invokes the true native halt method.
*/
static void halt(int status) {
synchronized (haltLock) {
halt0(status);
}
}

static native void halt0(int status);

底层直接调用了JNI方法实现JVM进程的强制关闭,整个链路中没有钩子任务的执行逻辑。