java.lang

System

这个类看起来是 JavaLangAccess 的实现(虽然没有做过 implements 声明),所以注册钩子也可以用这个方法:

sun.misc.SharedSecrets.getJavaLangAccess().registerShutdownHook

identityHashCode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
Map map = new HashMap();
map.put(1, 2);
map.put(3, 4);
// 10
// 1746572565
System.out.println(map.hashCode());
System.out.println(System.identityHashCode(map));;

// 989110044
// 989110044
Object object = new Object();
System.out.println(object.hashCode());
System.out.println(System.identityHashCode(object));
}

Map 自己覆盖了 hashCode() 方法,但普通 Object 没有。

exit

1
2
3
4
5
6
7

// java.lang.System

// 通常这个方法需要传入非 0 值,代表 abnormal 结束。
public static void exit(int status) {
Runtime.getRuntime().exit(status);
}

这个方法的调用通常是不会正常返回的,也就是说在下面写什么代码都执行不到了,包括 finally。这产生了一个很经典的陷阱,如果在这样的场景下

1
2
3
4
5
6
7
8
9
10
11
// 在主线程内
synchronized(lock) {
// 遇到故障
System.exit(1);
}

// 在钩子线程里
synchronized(lock) {
// 清理
}

因为线程死锁,清理钩子永远执行不完。而 exit 也会卡死在这里,所以是个只能进不能出的 虫洞(wormhole)。

改进方法是:

1
2
3
4
5
6
7
8
9
10
// 在主线程内
synchronized(lock) {
// 遇到故障

// 绕开这把锁
new Thread(()-> {
System.exit(1);
}).start();
// 因为上面不阻塞,所以下面的大括号退出后这把锁就会被归还
}

Runtime

addShutdownHook

1
2
3
4
5
6
7
8
// 这个方法内部直接去 Shutdown 这个内部类做注册
public void addShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
ApplicationShutdownHooks.add(hook);
}

exit

initiating its shutdown sequence 初始化关闭流程用

The virtual machine’s shutdown sequence consists of two phases:

  1. shutdown hooks are started in some unspecified order and allowed to run concurrently until they finish.
  2. all uninvoked finalizers are run if finalization-on-exit has been enabled. Once this is done the virtual machine.

然后 jvm halts。

第一次调用这个方法触发关闭钩子运行的话,第二次调用这个方法则会无限阻塞(下面我们会看到这是怎么实现的);如果此时钩子运行完,而 on-exit finalization 被激活,前一个方法exit 非 0,则第二次调用 halts,否则无限阻塞(blocks indefinitely)。

Runtime -> Shutdown 来关闭。

这是一个包级别的类,主要是为了 Runtime 服务的。

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
 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) {
// 这里是调用 native 方法,不知道为什么调用在 sequence 前
runAllFinalizers();
// 上面和下面调用的是同一个 runAllFinalizers,如果在这里的 halt 导致了退出,则下面的钩子理论上是不会被执行的
halt(status);
}
synchronized (Shutdown.class) {
/* Synchronize on the class object, causing any other thread
* that attempts to initiate shutdown to stall indefinitely
*/
beforeHalt();
// 真正调用的关闭钩子和 finalizer 的地方
sequence();
// 在这里停车
halt(status);
}
}


/* 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.
*/
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;
}
// 运行所有的 finalizer,这是个 native 方法
if (rfoe) runAllFinalizers();
}

/* Run all registered shutdown hooks
*/
private static void runHooks() {
for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
try {
Runnable hook;
// 这个锁很重要,目的是通过Happens-Before保证内存的可见性,理论上读写 volatile 变量也可以达到这样的效果,不过这个代码是 java 1.3 时代的产物了
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;
}
}
}
}

Shutdown 维护一个 hooks 数组,每次遍历一个数组求一次锁。

但 Runtime 增加钩子是通过一个来自于 Runable 的 ApplicationShutdownHooks 的 add,它内部维护了一个 IdentityHashMap hooks。真正执行的 ApplicationShutdownHooks 是一个单例,它在初始化的时候自注册到 Shutdown 的 hooks 数组里,然后执行的时候对每个线程进行按顺序的同步 start。

但:

  1. 线程钩子的注册时机不一样。
  2. 线程的 start 的实际执行流程也是未定的。

所以钩子是乱序执行的。

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
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 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 {
// 只有所有的 thread 都执行完,才退出这个本方法
hook.join();
break;
// 这个 ignored 是个好名字
} catch (InterruptedException ignored) {
}
}
}
}

另外,Shutdown.add 的存在,允许从其他入口注册多个钩子。

ApplicationShutdownHooks 还有相应的几个兄弟:DeleteOnExitHook、Console restore hook。这些兄弟在 Open JDK 里是有的,但 Kona JDK 就没有。