Java应用如何使用shutdownhook

Java应用如何使用shutdownhook

在线上Java程序中经常遇到进程程挂掉,一些状态没有正确的保存下来,这时候就需要在JVM关掉的时候执行一些清理现场的代码。JDK在1.3之后提供了Java Runtime.addShutdownHook(Thread hook)方法,可以注册一个JVM关闭的钩子,这个钩子可以在以下几种场景被调用:

  1. 程序正常退出
  2. 使用System.exit()
  3. 终端使用Ctrl+C触发的中断
  4. 系统关闭
  5. 使用Kill pid命令干掉进程(kill -9 除外)

关闭钩子在以下情景不会被调用:

  • 通过kill -9命令杀死进程——所以kill -9一定要慎用;
  • 程序中执行到了Runtime.getRuntime().halt()方法,该方法会强行关闭虚拟机;
  • 操作系统突然崩溃,或机器掉电。

一旦开始执行ShutdownHook时,无法再向JVM中addShutdownHook。

在JDK中方法的声明:

1
2
3
4
5
6
7
public void addShutdownHook(Thread hook)
参数
hook -- 一个初始化但尚未启动的线程对象,注册到JVM钩子的运行代码。
异常
IllegalArgumentException -- 如果指定的钩已被注册,或如果它可以判定钩已经运行或已被运行
IllegalStateException -- 如果虚拟机已经是在关闭的过程中
SecurityException -- 如果存在安全管理器并且它拒绝的RuntimePermission(“shutdownHooks”)

中断信号

在Linux信号机制中,存在多种进程中断信号(Linux信号列表 )。其中比较典型的有 SIGNKILL(9) 和 SIGNTERM(15).

SIGNKILL(9) 和 SIGNTERM(15) 的区别在于:
SIGNKILL(9) 的效果是立即杀死进程. 该信号不能被阻塞, 处理和忽略。
SIGNTERM(15) 的效果是正常退出进程,退出前可以被阻塞或回调处理。并且它是Linux缺省的程序中断信号。

注:在使用kill -9 pid 时JVM注册的钩子不会被调用。 kill 命令默认信号是 -15 ,-9 时会强制关闭。通常在使⽤ kill -9 前,应该先使⽤ kill -15,给⽬标进程⼀个清理善后⼯作的机会。如果没有,可能会留下⼀些不完整的⽂件或状态,从⽽影响服务的再次启动。

demo

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
public class TestShutdownHook {
static Timer timer = new Timer("job-timer");
static AtomicInteger count = new AtomicInteger(0);

/**
* hook线程
*/
static class CleanWorkThread extends Thread{
@Override
public void run() {
System.out.println("clean some work.");
timer.cancel();
try {
System.out.println("try sleep 2s.");
Thread.sleep(2 * 1000);
System.out.println("sleep 2s.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
//将hook线程添加到运行时环境中去
Runtime.getRuntime().addShutdownHook(new CleanWorkThread());
System.out.println("ShutdownHook added");
//简单模拟
timer.schedule(new TimerTask() {
@Override
public void run() {
count.getAndIncrement();
System.out.println("doing job " + count);
if (count.get() == 3) { //执行了3次后退出
System.exit(0);
}
}
}, 0, 2 * 1000);
}
}

output:

1
2
3
4
5
6
7
ShutdownHook added
doing job 1
doing job 2
doing job 3
clean some work.
try sleep 2s.
sleep 2s.

源码分析

Runtime类中对ShutdownHook的操作都是通过工具类ApplicationShutdownHooks来实现的。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class ApplicationShutdownHooks {
// IdentityHashMap的特性是 比较键(和值)时使用引用相等性代替对象相等性
// 也就是说使用 == 而不是使用 equals,即允许存在值相同,但是引用不同的情况
/* The set of registered hooks */
private static IdentityHashMap<Thread, Thread> hooks;
static {
try {
// 注册根shutdown(shutdown 槽)
Shutdown.add(1 /* shutdown hook 调用顺序 */,
false /* not registered if shutdown in progress,默认为false */,
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;
}
}

private ApplicationShutdownHooks() {}
// 添加钩子
static synchronized void add(Thread hook) {
// shutdown hooks执行中无法添加新钩子
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 synchronized boolean remove(Thread hook) {
if(hooks == null)
throw new IllegalStateException("Shutdown in progress");

if (hook == null)
throw new NullPointerException();

return hooks.remove(hook) != null;
}

/* 遍历所有 application hooks 并启动线程*/
static void runHooks() {
Collection<Thread> threads;
synchronized(ApplicationShutdownHooks.class) {
threads = hooks.keySet();
hooks = null;
}

for (Thread hook : threads) {
hook.start(); // 此处说明shutdown hook 是乱序执行的
}
for (Thread hook : threads) {
while (true) {
try {
hook.join(); // hook 线程阻塞当前线程
break;
} catch (InterruptedException ignored) {
}
}
}
}
}

Shutdown

下面来看下Shutdown的源码

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
class Shutdown {
// Shutdown 状态 枚举
private static final int RUNNING = 0;
private static final int HOOKS = 1;
private static final int FINALIZERS = 2;
// 存储 Shutdown状态
private static int state = RUNNING;

// 是否退出时执行所有回收器
private static boolean runFinalizersOnExit = false;

// shutdown hook的 预注册 逻辑:
// (0) Console restore hook :处理命令行
// (1) Application hooks
// (2) DeleteOnExit hook
private static final int MAX_SYSTEM_HOOKS = 10; // 最大hook slot 数是10
private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS];

// 当前运行的hook在数组中的位置
private static int currentRunningHook = 0;

// 保护 static 字段的锁类
private static class Lock { };
// 类锁,用于处理hooks 数组
private static Object lock = new Lock();

/* 类锁的key,用于保证halt方法的并发 */
private static Object haltLock = new Lock();

/* 该方法被 Runtime.runFinalizersOnExit() 调用 */
static void setRunFinalizersOnExit(boolean run) {
synchronized (lock) {
runFinalizersOnExit = run;
}
}

/**
* 添加新的shutdown,检查状态和配置
*
* 除了注册DeleteOnExitHook的时侯,registerShutdownInProgress 参数默认设为false
* application shutdown hooks 在运行中的时候,可能会向DeleteOnExitHook添加文件
* File.deleteOnExit() 会调用 DeleteOnExitHook.add(path);
*
* @params slot 退出钩子的索引位置, 在shutdown期间按顺序执行
* @params registerShutdownInProgress true to allow the hook
* to be registered even if the shutdown is in progress.
* @params hook the hook to be registered
*
* @throw IllegalStateException
* if registerShutdownInProgress is false and shutdown is in progress; or
* if registerShutdownInProgress is true and the shutdown process
* already passes the given slot
*/
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 {
// 1. 已经执行完了hook 阶段
// 2. 还在处理hook阶段,但是已经处理完了这个slot
if (state > HOOKS || (state == HOOKS && slot <= currentRunningHook))
throw new IllegalStateException("Shutdown in progress");
}

hooks[slot] = hook;
}
}

/* 运行所有的shutdownhook
*/
private static void runHooks() {
for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
try {
Runnable hook;
synchronized (lock) {
// 确保数组的可见性
currentRunningHook = i;
hook = hooks[i];
}
if (hook != null) hook.run(); // 注意此处是顺序执行的
// 使用的是run,而不是start()
} catch(Throwable t) {
if (t instanceof ThreadDeath) {
ThreadDeath td = (ThreadDeath)t;
throw td;
}
}
}
}
// 强制结束,status是非零退出码
static void halt(int status) {
synchronized (haltLock) {
// 此处加类锁,是为了防止处理 delete-on-shutdown 文件列表时的并发问题
halt0(status);
}
}
static native void halt0(int status);

/* (虫洞)实际上等同调用 java.lang.ref.Finalizer.runAllFinalizers */
private static native void runAllFinalizers();


/* 真正的shutdown逻辑.
* 如果不是为了支持 runFinalizersOnExit,这个方法会很简单,直接执行所有的hooks,然后 halt。
* 现在我们需要关注当前是在执行 hooks 或 finalizers。
* 在第二个用例,finalizer 可能直接调用exit(1) ,从而立刻终止。
* 但是在第一个用例,任何对exit(n)的调用都会失效。
* 注意,如果开启了on-exit finalizers,只有当 shutdown exit(0) 时才会执行。
*/
private static void sequence() {
synchronized (lock) {
/* 防止守护线程在jvm 执行shutdown后调用exit()
*/
if (state != HOOKS) return;
}
runHooks(); // 运行钩子方法
boolean rfoe;
synchronized (lock) {
state = FINALIZERS; // 此处修改了state
rfoe = runFinalizersOnExit;
}
if (rfoe) runAllFinalizers(); // 执行所有的 finalizer
}


/* 被Runtime.exit()调用, 或者被系统终止事件handler调用。
*/
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) {
// 非0状态立即停止
halt(status);
} else {
// 兼容旧逻辑,执行finalizer后退出
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);
}
}


/* 通过JNI 调用
* 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的字段state的状态机如下图所示:

graph LR
id0((begin))-->id1(running)
id1-->|"Runtime.exit()"|id2(HOOKS)
id1-->|"shutdown()"|id2
id2-->|"sequence()"|id3(FINALIZERS)
id3-->id4((end))

delete-on-exit file list

java.io.File的deleteOnExit()方法可以在JVM退出时,删除文件。主要的实现就是DeleteOnExitHook.

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
40
41
42
43
44
45
46
class DeleteOnExitHook {
private static LinkedHashSet<String> files = new LinkedHashSet<>();
static {
// DeleteOnExitHook 必须是最后一个被调用的shutdown hook。
// Application shutdown hooks 可能会向 delete on exit file list中添加文件,
// 此时shutdown的状态是 hooks, 所以将 registerShutdownInProgress 设置为 true.
sun.misc.SharedSecrets.getJavaLangAccess()
.registerShutdownHook(2 /* Shutdown hook invocation order */,
true /* register even if shutdown in progress */,
new Runnable() {
public void run() {
runHooks();
}
}
);
}

private DeleteOnExitHook() {}

static synchronized void add(String file) {
if(files == null) {
// DeleteOnExitHook is running. Too late to add a file
throw new IllegalStateException("Shutdown in progress");
}

files.add(file);
}

static void runHooks() {
LinkedHashSet<String> theFiles;

synchronized (DeleteOnExitHook.class) {
theFiles = files;
files = null;
}

ArrayList<String> toBeDeleted = new ArrayList<>(theFiles);

// reverse the list to maintain previous jdk deletion order.
// Last in first deleted.
Collections.reverse(toBeDeleted);
for (String filename : toBeDeleted) {
(new File(filename)).delete();
}
}
}