jvm關閉

關閉方式

正常關閉

  • 最後一個普通線程(非守護線程)結束
  • 調用了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中的執行流程爲:

  1. 獲取ApplicationShutdownHooks類鎖,然後然後把所有鉤子線程轉移出來,然後hooks置爲空
  2. 啓動所有鉤子線程
  3. 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;
    }
}

執行過程
最核心的執行方法爲,流程是

  1. 獲取鎖,判斷當前狀態是否處於HOOKS狀態(正在執行關閉鉤子),如果是直接退出
  2. 執行所有關閉鉤子任務,注意這裏不是啓動線程,而只是單純的調用方法
  3. 更新當前狀態到FINALIZERS
  4. 判斷是否需要執行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關閉流程

  1. ApplicationShutdownHooks初始化的時候,把自身runHooks方法作爲鉤子任務註冊到Shutdown中
  2. 用戶調用Runtime.getRuntime().addShutdownHook,註冊鉤子線程到ApplicationShutdownHooks中
  3. 當 System.exit or Runtime.exit or 接收到SIGINT,SIGTERM信號,將觸發Shutdown.exit方法;或者 所有普通線程都結束的時候 觸發 Shutdown.shutdown方法;這兩個方法,都會讓Shutdown執行所有註冊到自身的鉤子任務(非線程)
  4. Shutdown執行鉤子任務的時候,會執行ApplicationShutdownHooks的鉤子任務runHooks方法, 這時會啓 動所有註冊到ApplicationShutdownHooks的鉤子線程
  5. ApplicationShutdownHooks.runHooks主線程會等待所有鉤子線程執行完成才退出
  6. Shutdown執行完所有鉤子任務後,如果是Shutdown.exit方法,最後會執行halt方法關閉JVM
  7. 如果是調用Runtime.halt or 接收到SIGKILL信號,則直接關閉JVM

自問自答

正常關閉的幾個方法中,所有普通線程都結束的情況下,爲什麼最後不需要調用halt方法?
雖然源碼中沒有說明,但猜測是,因爲其他情況下,普通線程還在執行中,所以需要通過halt方法把其他線程一併殺掉

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章