在线上Java程序中经常遇到进程程挂掉,一些状态没有正确的保存下来,这时候就需要在JVM关掉的时候执行一些清理现场的代码。JDK在1.3之后提供了Java Runtime.addShutdownHook(Thread hook)方法,可以注册一个JVM关闭的钩子,这个钩子可以在以下几种场景被调用:
程序正常退出 使用System.exit() 终端使用Ctrl+C触发的中断 系统关闭 使用Kill pid命令干掉进程(kill -9|19 除外) 关闭钩子在以下情景不会被调用:
通过kill -9命令杀死进程——所以kill -9|19 一定要慎用; 程序中执行到了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),SIGSTOP(19) 和 SIGNTERM(15).
SIGNKILL(9),SIGSTOP(19) 和 SIGNTERM(15)的区别在于: SIGNKILL(9),SIGSTOP(19) 的效果是立即杀死进程. 该信号不能被阻塞, 处理和忽略。 SIGNTERM(15) 的效果是正常退出进程,退出前可以被阻塞或回调处理。并且它是Linux缺省的程序中断信号。
注:在使用kill -9|19 pid 时JVM注册的钩子不会被调用。 kill 命令默认信号是 -15 ,-9|19 时会强制关闭。通常在使⽤ kill -9|19 前,应该先使⽤ 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 ); 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 { 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 ) { 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 class ApplicationShutdownHooks { private static IdentityHashMap<Thread, Thread> hooks; static { try { Shutdown.add(1 , false , new Runnable () { public void run () { runHooks(); } } ); hooks = new IdentityHashMap <>(); } catch (IllegalStateException e) { hooks = null ; } } private ApplicationShutdownHooks () {} 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 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 ; } 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) { } } } } }
ApplicationShutdownHooks 类中也有一个集合IdentityHashMap<Thread, Thread> hooks
,专门用来存放由用户自定义的 Application hooks 类型的 ShutdownHook 。通过 ApplicationShutdownHooks#add 方法添加进 hooks 集合中。
然后在 runHooks 方法里挨个启动 ShutdownHook 线程,并发执行。注意这里的 runHooks 方法是 ApplicationShutdownHooks 类中的。
在 ApplicationShutdownHooks 类的静态代码块中会将 runHooks 方法封装成 Runnable 添加进 Shutdown 类中的 hooks 数组中。注意这里 Shutdown#add 方法传递进的索引是 1 。
Shutdown 下面来看下Shutdown的源码。Shutdown 类中,包含了一个 Runnable[]
hooks 数组,容量为 10 。JDK 中的 ShutdownHook 是以类型来分类的,数组 hooks 每一个槽中存放的是一种特定类型的 ShutdownHook 。
(0) Console restore hook :处理命令行 (1) Application hooks (2) DeleteOnExit hook 在程序代码中通过 Runtime.getRuntime().addShutdownHook 注册的是 Application hooks 类型的 ShutdownHook ,存放在数组 hooks 中索引为 1 的槽中。
参数 registerShutdownInProgress 表示是否允许在 JVM 关闭流程开始之后,继续向 JVM 添加 ShutdownHook 。默认为 false 表示不允许。否则将会抛出 IllegalStateException 异常。
sequenceDiagram
Shutdown->>+Shutdown: runHooks
Shutdown->>+Console: console restore hooks run
Console-->>-Shutdown: finish
Shutdown->>+ApplicationShutdownHooks: runHooks
loop Every ShutdownHook
ApplicationShutdownHooks->>+ShutdownHook: start
ShutdownHook-->>-ApplicationShutdownHooks: join
end
ApplicationShutdownHooks-->>-Shutdown: finish
Shutdown->>+DeleteOnExitHook: runHooks
loop Every toBeDeletedFile
DeleteOnExitHook->>+toBeDeletedFile: delete
toBeDeletedFile-->>-DeleteOnExitHook: finish
end
DeleteOnExitHook-->>-Shutdown: finish 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 class Shutdown { private static final int RUNNING = 0 ; private static final int HOOKS = 1 ; private static final int FINALIZERS = 2 ; private static int state = RUNNING; private static boolean runFinalizersOnExit = false ; private static final int MAX_SYSTEM_HOOKS = 10 ; private static final Runnable[] hooks = new Runnable [MAX_SYSTEM_HOOKS]; private static int currentRunningHook = 0 ; private static class Lock { }; private static Object lock = new Lock (); private static Object haltLock = new Lock (); static void setRunFinalizersOnExit (boolean run) { synchronized (lock) { runFinalizersOnExit = run; } } 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; } } 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(); } catch (Throwable t) { if (t instanceof ThreadDeath) { ThreadDeath td = (ThreadDeath)t; throw td; } } } } static void halt (int status) { synchronized (haltLock) { halt0(status); } } static native void halt0 (int status) ; private static native void runAllFinalizers () ; private static void sequence () { synchronized (lock) { if (state != HOOKS) return ; } runHooks(); boolean rfoe; synchronized (lock) { state = FINALIZERS; rfoe = runFinalizersOnExit; } if (rfoe) runAllFinalizers(); } static void exit (int status) { boolean runMoreFinalizers = false ; synchronized (lock) { if (status != 0 ) runFinalizersOnExit = false ; switch (state) { case RUNNING: state = HOOKS; break ; case HOOKS: break ; case FINALIZERS: if (status != 0 ) { halt(status); } else { runMoreFinalizers = runFinalizersOnExit; } break ; } } if (runMoreFinalizers) { runAllFinalizers(); halt(status); } synchronized (Shutdown.class) { sequence(); halt(status); } } static void shutdown () { synchronized (lock) { switch (state) { case RUNNING: state = HOOKS; break ; case HOOKS: case FINALIZERS: break ; } } synchronized (Shutdown.class) { sequence(); } } }
shutdown的字段state的状态机如下图所示:
graph LR
id0((begin))-->id1(running)
id1-->|"ShutDown#exit()"|id2(HOOKS)
id1-->|"ShutDown#shutdown()"|id2
id2-->|"sequence()"|id3(FINALIZERS)
id3-->id4((end)) 触发ShowdownHook执行的原因有3个:
最后一个非收获进程退出时,通过JNI DestroyJavaVM
调用 Shutdown#shutdown
调用Runtime.exit(status)
或System.exit(status)
系统信号Terminator(status不为0) 注意,Shutdown#exit(status)的参数不为0时,Finalizers不会运行
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 { sun.misc.SharedSecrets.getJavaLangAccess() .registerShutdownHook(2 , true , new Runnable () { public void run () { runHooks(); } } ); } private DeleteOnExitHook () {} static synchronized void add (String file) { if (files == null ) { 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); Collections.reverse(toBeDeleted); for (String filename : toBeDeleted) { (new File (filename)).delete(); } } }