1. 程式人生 > >如何為JVM新增關閉鉤子與簡要分析

如何為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類中。

static {
    try {
        Shutdown.add(1, false,
            new Runnable() {
                public void run() {
                    runHooks();
                }
            }
        );
        hooks = new IdentityHashMap<>();
    } catch (IllegalStateException e) {
        hooks = null;
    }
}
Shutdown類裡也維護了一個鉤子集合
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>