關閉方式
正常關閉
- 最後一個普通線程(非守護線程)結束
- 調用了System.exit
- 發送SIGINT信號(相當於不帶參數的kill命令)或者鍵入Ctrl-C
強制關閉
- 調用Runtime.halt
- 發送SIGKILL信號(kill -9 命令)
關閉鉤子(Shutdown Hook)
鉤子配置方法
通過下面的設置方法可看到,關閉鉤子實際爲線程
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
}));
觸發調用Hook線程流程
正常關閉中,JVM首先調用所有已註冊的關閉鉤子。JVM並不能保證關閉鉤子的調用順序。在關閉應用程序線程時,如果有線程仍然在運行,那麼這些線程接下來將與關閉進程併發進行。
當所有的關閉鉤子都執行結束時,如果runFinalizersOnExit爲true,那麼JVM將運行終結器,然後再停止。JVM並不會停止或中斷任何在關閉時仍然運行的應用程序線程。當JVM最終結束時,這些線程將被強行結束。
如果關閉鉤子或終結器沒有執行完成,那麼正常關閉進程掛起並且JVM必須被強行關閉。
–《Java併發編程實戰》
上述是書裏面的介紹,下面跟着源碼對照整個流程:
添加鉤子
Runtime:
public void addShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
ApplicationShutdownHooks.add(hook);
}
ApplicationShutdownHooks:
會檢查hook線程是否被啓動,是否已經添加過
其中看到第一個判斷條件是判斷hooks是否爲空,這是因爲當ApplicationShutdownHooks開始啓動鉤子線程後,會把hooks置爲空,也意味着開始啓動鉤子線程後,就再也無法添加鉤子線程
/* Add a new shutdown hook. Checks the shutdown state and the hook itself,
* but does not do any security checks.
*/
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);
}
關閉鉤子管理器的初始化
從上面代碼可以看到,實際管理關閉鉤子的是ApplicationShutdownHooks類,
可以看到ApplicationShutdownHooks其實是屬於 用戶/應用程序等級(user level) 的關閉鉤子管理器(註釋),該類初始化的時候,會把自己註冊到虛擬機的關閉鉤子隊列中(Shutdown)(靜態初始代碼)
初始化:
/*
* Class to track and run user level shutdown hooks registered through
* <tt>{@link Runtime#addShutdownHook Runtime.addShutdownHook}</tt>.
*
* @see java.lang.Runtime#addShutdownHook
* @see java.lang.Runtime#removeShutdownHook
*/
class ApplicationShutdownHooks {
/* The set of registered hooks */
private static IdentityHashMap<Thread, Thread> hooks;
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;
}
}
...
}
關閉鉤子的調用
因爲ApplicationShutdownHooks自己也是作爲一個關閉鉤子註冊到JVM的關閉鉤子隊列中,所以也是由Shutdown來觸發,下面先看ApplicationShutdownHooks中的執行流程爲:
- 獲取ApplicationShutdownHooks類鎖,然後然後把所有鉤子線程轉移出來,然後hooks置爲空
- 啓動所有鉤子線程
- join所有鉤子線程
第一個步驟,把hooks置爲空後,在添加或者移除鉤子的時候,會拋出異常,無法添加或者移除鉤子,即當ApplicationShutdownHooks開始啓動關閉鉤子線程後,不能再添加、移除鉤子
第二個步驟,啓動線程併發執行,驗證了書裏面說的不能保證關閉鉤子的調用順序
執行:
/* Iterates over all application hooks creating a new thread for each
* to run in. Hooks are run concurrently and this method waits for
* them to finish.
*/
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) {
}
}
}
}
Shutdown解析
類功能
Shutdown是用於管理JVM關閉
/**
* Package-private utility class containing data structures and logic
* governing the virtual-machine shutdown sequence.
*
* @author Mark Reinhold
* @since 1.3
*/
class Shutdown {...}
添加鉤子
再來看看添加鉤子的方法,傳遞3個參數:slot,registerShutdownInProgress,hook
slot:存放關閉鉤子的是一個固定大小的數組,並且每個槽位存放鉤子是固定的,以及只能被初始化一次
registerShutdownInProgress:決定是否允許Shutdown開始執行關閉鉤子線程後添加鉤子,而ApplicationShutdownHooks在註冊的時候傳入的是false,因此如果Shutdown開始執行關閉鉤子線程後,添加鉤子線程會拋出IllegalStateException異常
/* Shutdown state */
private static final int RUNNING = 0;
private static final int HOOKS = 1;
private static final int FINALIZERS = 2;
private static int state = RUNNING;
// The system shutdown hooks are registered with a predefined slot.
// The list of shutdown hooks is as follows:
// (0) Console restore hook
// (1) Application hooks
// (2) DeleteOnExit hook
private static final int MAX_SYSTEM_HOOKS = 10;
private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS];
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;
}
}
執行過程
最核心的執行方法爲,流程是
- 獲取鎖,判斷當前狀態是否處於HOOKS狀態(正在執行關閉鉤子),如果是直接退出
- 執行所有關閉鉤子任務,注意這裏不是啓動線程,而只是單純的調用方法
- 更新當前狀態到FINALIZERS
- 判斷是否需要執行finalize方法,如果需要則執行 P.S. runFinalizersOnExit參數可通過set方法設置
正如前面提到,JVM正常關閉的幾個觸發點,都會觸發調用該方法
p.s. 關於runFinalizersOnExit參數,由目前代碼來看,該參數已被棄用
/* Run all registered shutdown hooks
*/
private static void runHooks() {
for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
try {
Runnable hook;
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;
}
}
}
}
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;
}
if (rfoe) runAllFinalizers();
}
鉤子觸發
當所有普通線程都結束的時候,會觸發該shutdown方法(被DestroyJavaVM調用),執行關閉鉤子,而且該方法不會關掉JVM
p.s. 唯一的普通線程出現未捕獲異常而退出,其實也屬於這種情況,因此也會調用shutdown方法
/* 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();
}
}
另外就是調用了System.exit,以及接收到SIGINT,SIGTERM信號時的處理,這兩種觸發底層實際都爲調用Shutdown.exit方法,其中執行完finalize方法以及關閉鉤子後,纔會執行halt方法關閉JVM,驗證了書中所說,終結器和關閉鉤子未完成的情況下, JVM不會關閉退出。
可以看到,只有在傳入的status爲0的情況下(正常退出),纔會執行runAllFinalizers方法(終結器)
/* Invoked by Runtime.exit, which does all the security checks.
* Also invoked by handlers for system-provided termination events,
* which should pass a nonzero status code.
*/
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) {
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);
}
}
System.exit的調用:
public final class System {
...
public void exit(int status) {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkExit(status);
}
Shutdown.exit(status);
}
...
}
處理SIGINT,SIGTERM信號的調用,具體來說,下面的代碼不是觸發調用的地方,根據註釋,Terminator屬於負責 安裝和卸載 處理平臺終端信息處理器,並由System類在線程初始化後調用(Initialize the system class. Called after thread initialization.)。
從方法也可以看到,提供了setup和teardown兩個方法,其中,setup中可以看到,針對INT和TERM信號處理是調用Shutdown.exit,後面流程大概就是接受到INT和TERM信號,然後調用相應的處理器進行處理(沒有看到相關代碼)
/**
* Package-private utility class for setting up and tearing down
* platform-specific support for termination-triggered shutdowns.
*
* @author Mark Reinhold
* @since 1.3
*/
class Terminator {
private static SignalHandler handler = null;
/* Invocations of setup and teardown are already synchronized
* on the shutdown lock, so no further synchronization is needed here
*/
static void setup() {
if (handler != null) return;
SignalHandler sh = new SignalHandler() {
public void handle(Signal sig) {
Shutdown.exit(sig.getNumber() + 0200);
}
};
handler = sh;
// When -Xrs is specified the user is responsible for
// ensuring that shutdown hooks are run by calling
// System.exit()
try {
Signal.handle(new Signal("INT"), sh);
} catch (IllegalArgumentException e) {
}
try {
Signal.handle(new Signal("TERM"), sh);
} catch (IllegalArgumentException e) {
}
}
static void teardown() {
/* The current sun.misc.Signal class does not support
* the cancellation of handlers
*/
}
}
Signal中的觸發方法
private static void dispatch(int var0) {
final Signal var1 = (Signal)signals.get(var0);
final SignalHandler var2 = (SignalHandler)handlers.get(var1);
Runnable var3 = new Runnable() {
public void run() {
var2.handle(var1);
}
};
if (var2 != null) {
(new Thread(var3, var1 + " handler")).start();
}
}
強制關閉
在開始時候有提到的強制關閉的幾種方法,其中Runtime.halt,看源碼可知其實際調用的爲Shutdown.halt方法,即不調用關閉鉤子,直接關閉JVM
static void halt(int status) {
synchronized (haltLock) {
halt0(status);
}
}
SIGKILL信號的處理,沒看到哪裏有註冊相關處理器的代碼
總結
最後總結下jvm關閉的相關流程
JVM關閉流程
- ApplicationShutdownHooks初始化的時候,把自身runHooks方法作爲鉤子任務註冊到Shutdown中
- 用戶調用Runtime.getRuntime().addShutdownHook,註冊鉤子線程到ApplicationShutdownHooks中
- 當 System.exit or Runtime.exit or 接收到SIGINT,SIGTERM信號,將觸發Shutdown.exit方法;或者 所有普通線程都結束的時候 觸發 Shutdown.shutdown方法;這兩個方法,都會讓Shutdown執行所有註冊到自身的鉤子任務(非線程)
- Shutdown執行鉤子任務的時候,會執行ApplicationShutdownHooks的鉤子任務runHooks方法, 這時會啓 動所有註冊到ApplicationShutdownHooks的鉤子線程
- ApplicationShutdownHooks.runHooks主線程會等待所有鉤子線程執行完成才退出
- Shutdown執行完所有鉤子任務後,如果是Shutdown.exit方法,最後會執行halt方法關閉JVM
- 如果是調用Runtime.halt or 接收到SIGKILL信號,則直接關閉JVM
自問自答
正常關閉的幾個方法中,所有普通線程都結束的情況下,爲什麼最後不需要調用halt方法?
雖然源碼中沒有說明,但猜測是,因爲其他情況下,普通線程還在執行中,所以需要通過halt方法把其他線程一併殺掉