如何為JVM新增關閉鉤子與簡要分析
摘要
最近在看噹噹開源的資料庫分庫分表框架Sharding-jdbc的原始碼,在看ExecutorEngine類時,遇到了很多沒用過的JDK api,Sharding-jdbc內部大量的使用了google的工具包Guava。在ExecutorEngine類處理多執行緒問題部分也同樣用到的Guava下面的util.concurrent包的類進處理。而我在看google的Guava的MoreExecutors時便遇到了Runtime.getRuntime().addShutdownHook(hook)。
1、JVM的關閉鉤子
JVM的關閉鉤子是通過Runtime#addShutdownHook(Thread hook)方法來實現的,根據api是註解可知所謂的 shutdown hook 就是一系例的已初始化但尚未執行的執行緒物件。
當準備JVM停止前,這些shutdown hook 執行緒會被執行。以下幾種情況會使這個shutdown hook呼叫:
程式正常退出,這發生在最後的非守護執行緒退出時,或者在呼叫 exit(等同於System.exit)方法。
為響應使用者中斷而終止 虛擬機器,如鍵入 ^C;或發生系統事件,比如使用者登出或系統關閉。
註冊jvm關閉鉤子通過Runtime.addShutdownHook(),實際呼叫ApplicationShutdownHooks.add()。後者維護了一個鉤子集合IdentityHashMap<Thread, Thread> hooks。
<span style="font-size:18px;">public void addShutdownHook(Thread hook) { SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPermission(new RuntimePermission("shutdownHooks")); } ApplicationShutdownHooks.add(hook); }</span>
ApplicationShutdownHooks類初始化的時候,會呼叫static塊註冊一個執行緒到Shutdown類中。
Shutdown類裡也維護了一個鉤子集合static { try { Shutdown.add(1, false, new Runnable() { public void run() { runHooks(); } } ); hooks = new IdentityHashMap<>(); } catch (IllegalStateException e) { hooks = null; } }
private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS];
這個集合是分優先順序的(優先順序就是下標數值),自定義的鉤子優先順序預設是1,也就是最先執行。關閉鉤子最終觸發就是從這個集合進行。應用關閉時,以System.exit()為例,依次呼叫Runtime.exit()、Shutdow.exit()。
Shutdown執行jvm退出邏輯,並維護了若干關閉狀態
private static final int RUNNING = 0; // 初始狀態,開始關閉
private static final int HOOKS = 1; // 執行鉤子
private static final int FINALIZERS = 2; // 執行finalizer
private static int state = RUNNING;
static void exit(int status) {
boolean runMoreFinalizers = false;
synchronized (lock) { // 根據退出碼status引數做不同處理
if (status != 0) runFinalizersOnExit = false; // 只有正常退出才會執行finalizer
switch (state) {
case RUNNING: // 執行鉤子並修改狀態
state = HOOKS;
break;
case HOOKS: // 執行鉤子
break;
case FINALIZERS: // 執行finalizer
if (status != 0) {
halt(status); // 如果是異常退出,直接退出程序。halt()底層是native實現,這時不會執行finalizer
} else { // 正常退出則標記是否需要執行finalizer
runMoreFinalizers = runFinalizersOnExit;
}
break;
}
}
if (runMoreFinalizers) { // 如果有需要,就執行finalizer,注意只有state=FINALIZERS會走這個分支
runAllFinalizers();
halt(status);
}
synchronized (Shutdown.class) {
// 這裡執行state= HOOKS邏輯,包括執行鉤子和finalizer
sequence();
halt(status);
}
}
private static void sequence() {
synchronized (lock) {
if (state != HOOKS) return;
}
runHooks(); // 執行鉤子,這裡會依次執行hooks數組裡的各執行緒
boolean rfoe; // finalizer邏輯
synchronized (lock) {
state = FINALIZERS;
rfoe = runFinalizersOnExit;
}
if (rfoe) runAllFinalizers();
}
private static void runHooks() {
for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
try {
Runnable hook;
synchronized (lock) {
currentRunningHook = i;
hook = hooks[i];
}
// 由於之前註冊了ApplicationShutdownHooks的鉤子執行緒,這裡又會回撥ApplicationShutdownHooks.runHooks
if (hook != null) hook.run();
} catch(Throwable t) {
if (t instanceof ThreadDeath) {
ThreadDeath td = (ThreadDeath)t;
throw td;
}
}
}
}
static void runHooks() {
Collection<Thread> threads;
synchronized(ApplicationShutdownHooks.class) {
threads = hooks.keySet();
hooks = null;
}
// 注意ApplicationShutdownHooks裡的鉤子之間是沒有優先順序的,如果定義了多個鉤子,那麼這些鉤子會併發執行
for (Thread hook : threads) {
hook.start();
}
for (Thread hook : threads) {
try {
hook.join();
} catch (InterruptedException x) { }
}
}
2、Spring關閉鉤子
Spring在AbstractApplicationContext裡維護了一個shutdownHook屬性,用來關閉Spring上下文。但這個鉤子不是預設生效的,需要手動呼叫ApplicationContext.registerShutdownHook()來開啟,在自行維護ApplicationContext(而不是託管給tomcat之類的容器時),注意儘量使用ApplicationContext.registerShutdownHook()或者手動呼叫ApplicationContext.close()來關閉Spring上下文,否則應用退出時可能會殘留資源。
public void registerShutdownHook() {
if (this.shutdownHook == null) {
this.shutdownHook = new Thread() {
@Override
public void run() {
// 這裡會呼叫Spring的關閉邏輯,包括資源清理,bean的銷燬等
doClose();
}
};
// 這裡會把spring的鉤子註冊到jvm關閉鉤子
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}
}
2、Hadoop關閉鉤子
Hadoop客戶端初始化時,org.apache.hadoop.util.ShutdownHookManager會向Runtime註冊一個鉤子執行緒。ShutdownHookManager是一個單例類,並維護了一個鉤子集合。private Set<HookEntry> hooks;
static {
Runtime.getRuntime().addShutdownHook(
new Thread() {
@Override
public void run() {
MGR.shutdownInProgress.set(true); // MGR是本類的單例
for (Runnable hook: MGR.getShutdownHooksInOrder()) {
try {
hook.run();
} catch (Throwable ex) {
LOG.warn("ShutdownHook '" + hook.getClass().getSimpleName() +
"' failed, " + ex.toString(), ex);
}
}
}
}
);
}
這裡HookEntry是hadoop封裝的鉤子類,HookEntry是帶優先順序的,一個priority屬性。MGR.getShutdownHooksInOrder()方法會按priority依次(單執行緒)執行鉤子。預設掛上的鉤子就一個:org.apache.hadoop.fs.FileSystem$Cache$ClientFinalizer(priority=10),這個鉤子用來清理hadoop FileSystem快取以及銷燬FileSystem例項。這個鉤子是在第一次hadoop IO發生時(如FileSystem.get)lazy載入此外呼叫FileContext.deleteOnExit()方法也會通過註冊鉤子hadoop叢集(非客戶端)啟動時,還會註冊鉤子清理臨時路徑。
4、SparkContext關閉鉤子
Spark也有關閉鉤子管理類org.apache.spark.util.ShutdownHookManager,結構與hadoop的ShutdownHookManager基本類似hadoop 2.x開始,spark的ShutdownHookManager會掛一個SparkShutdownHook鉤子到hadoop的ShutdownHookManager(priority=40),用來實現SparkContext的清理邏輯。hadoop 1.x沒有ShutdownHookManager,所以SparkShutdownHook直接掛在jvm上。def install(): Unit = {
val hookTask = new Runnable() {
// 執行鉤子的回撥程序,根據priority依次執行鉤子
override def run(): Unit = runAll()
}
Try(Utils.classForName("org.apache.hadoop.util.ShutdownHookManager")) match {
case Success(shmClass) =>
val fsPriority = classOf[FileSystem].getField("SHUTDOWN_HOOK_PRIORITY").get(null).asInstanceOf[Int]
val shm = shmClass.getMethod("get").invoke(null)
shm.getClass().getMethod("addShutdownHook", classOf[Runnable], classOf[Int]).invoke(shm, hookTask, Integer.valueOf(fsPriority + 30))
case Failure(_) => // hadoop 1.x
Runtime.getRuntime.addShutdownHook(new Thread(hookTask, "Spark Shutdown Hook"));
}
}
順便說一下,hadoop的FileSystem例項底層預設是複用的,所以如果執行了兩次fileSystem.close(),第二次會報錯FileSystem Already Closed異常(即使表面上是對兩個例項執行的)一個典型的場景是同時使用Spark和Hadoop-Api,Spark會建立FileSystem例項,Hadoop-Api也會建立,由於底層複用,兩者其實是同一個。因為關閉鉤子的存在,應用退出時會執行兩次FileSystem.close(),導致報錯。解決這個問題的辦法是在hdfs-site.xml增加以下配置,關閉FileSystem例項複用。
<property>
<name>fs.hdfs.impl.disable.cache</name>
<value>true</value>
</property>