1. 程式人生 > 其它 >Java優雅關服

Java優雅關服

技術標籤:Javajava

文章目錄


前言

java中的Runtime.addShutdownHook()可以新增鉤子函式在接收到程式關閉訊號的時候進行一些執行緒關閉資源回收的工作,也是一般實現優雅關服的基礎。我們來扒一扒這個shutdownHook的實現過程。

一、鉤子的新增和呼叫流程

1.Runtime#addShutdownHook

首先看看jdk對Runtime#addShutdownHook的說明。

/**
 * Registers a new virtual-machine shutdown hook.
 *
 * <p> The Java virtual machine <i>shuts down</i> in response to two kinds
 * of events:
 *
 *   <ul>
 *
 *   <li> The program <i>exits</i> normally, when the last non-daemon
 *   thread exits or when the <tt>{@link #exit exit}</tt> (equivalently,
 *   {@link System#exit(int) System.exit}) method is invoked, or
 *
 *   <li> The virtual machine is <i>terminated</i> in response to a
 *   user interrupt, such as typing <tt>^C</tt>, or a system-wide event,
 *   such as user logoff or system shutdown.
 *
 *   </ul>
 *
 * <p> A <i>shutdown hook</i> is simply an initialized but unstarted
 * thread.  When the virtual machine begins its shutdown sequence it will
 * start all registered shutdown hooks in some unspecified order and let
 * them run concurrently.  When all the hooks have finished it will then
 * run all uninvoked finalizers if finalization-on-exit has been enabled.
 * Finally, the virtual machine will halt.  Note that daemon threads will
 * continue to run during the shutdown sequence, as will non-daemon threads
 * if shutdown was initiated by invoking the <tt>{@link #exit exit}</tt>
 * method.
 *
 * <p> Once the shutdown sequence has begun it can be stopped only by
 * invoking the <tt>{@link #halt halt}</tt> method, which forcibly
 * terminates the virtual machine.
 *
 * <p> Once the shutdown sequence has begun it is impossible to register a
 * new shutdown hook or de-register a previously-registered hook.
 * Attempting either of these operations will cause an
 * <tt>{@link IllegalStateException}</tt> to be thrown.
 *
 * <p> Shutdown hooks run at a delicate time in the life cycle of a virtual
 * machine and should therefore be coded defensively.  They should, in
 * particular, be written to be thread-safe and to avoid deadlocks insofar
 * as possible.  They should also not rely blindly upon services that may
 * have registered their own shutdown hooks and therefore may themselves in
 * the process of shutting down.  Attempts to use other thread-based
 * services such as the AWT event-dispatch thread, for example, may lead to
 * deadlocks.
 *
 * <p> Shutdown hooks should also finish their work quickly.  When a
 * program invokes <tt>{@link #exit exit}</tt> the expectation is
 * that the virtual machine will promptly shut down and exit.  When the
 * virtual machine is terminated due to user logoff or system shutdown the
 * underlying operating system may only allow a fixed amount of time in
 * which to shut down and exit.  It is therefore inadvisable to attempt any
 * user interaction or to perform a long-running computation in a shutdown
 * hook.
 *
 * <p> Uncaught exceptions are handled in shutdown hooks just as in any
 * other thread, by invoking the <tt>{@link ThreadGroup#uncaughtException
 * uncaughtException}</tt> method of the thread's <tt>{@link
 * ThreadGroup}</tt> object.  The default implementation of this method
 * prints the exception's stack trace to <tt>{@link System#err}</tt> and
 * terminates the thread; it does not cause the virtual machine to exit or
 * halt.
 *
 * <p> In rare circumstances the virtual machine may <i>abort</i>, that is,
 * stop running without shutting down cleanly.  This occurs when the
 * virtual machine is terminated externally, for example with the
 * <tt>SIGKILL</tt> signal on Unix or the <tt>TerminateProcess</tt> call on
 * Microsoft Windows.  The virtual machine may also abort if a native
 * method goes awry by, for example, corrupting internal data structures or
 * attempting to access nonexistent memory.  If the virtual machine aborts
 * then no guarantee can be made about whether or not any shutdown hooks
 * will be run. <p>
 *
 * @param   hook
 *          An initialized but unstarted <tt>{@link Thread}</tt> object
 *
 * @throws  IllegalArgumentException
 *          If the specified hook has already been registered,
 *          or if it can be determined that the hook is already running or
 *          has already been run
 *
 * @throws  IllegalStateException
 *          If the virtual machine is already in the process
 *          of shutting down
 *
 * @throws  SecurityException
 *          If a security manager is present and it denies
 *          <tt>{@link RuntimePermission}("shutdownHooks")</tt>
 *
 * @see #removeShutdownHook
 * @see #halt(int)
 * @see #exit(int)
 * @since 1.3
 */
public void addShutdownHook(Thread hook) { SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPermission(new RuntimePermission("shutdownHooks")); } ApplicationShutdownHooks.add(hook); }

主要就是說明了這是個註冊鉤子的方法,這個鉤子會對兩類事件響應

  1. 一個是程式正常關閉,最後一個守護執行緒關閉或者Runtime.exit方法被呼叫。
  2. 一個是使用者的中斷操作如ctrl-c(linux會向程序傳送2-INT訊號)和系統關機(linux會向程序傳送15-TERM訊號)。

以及一些使用的注意事項,避免執行緒不安全導致的死鎖,鉤子裡的處理工作應儘快完成,在平臺上傳送一些特定的訊號如unix平臺上傳送SIGKILL(kill 9)訊號和widown平臺上的TerminateProcess會導致鉤子不能正常執行。

2.ApplicationShutdownHooks#add

在ApplicationShutdownHooks裡可以看到鉤子被新增到靜態變數hooks中了,hooks被runHooks方法呼叫,runHooks方法在ApplicationShutdownHooks類初始化的時候作為runable執行邏輯被Shutdown#add添加了。從runHooks方法呼叫鉤子的方式咱們可以看到鉤子是併發執行而不是序列的,所以新增鉤子的時候要注意不能有順序依賴。

/*
 * 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;
        }
    }
    
    /* 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) {
            try {
                hook.join();
            } catch (InterruptedException x) { }
        }
    }
    
    /* 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);
    }    

咱們接著看看Shutdown類是怎麼使用這個runable的

3.Shutdown#add

可以看到Shutdown#add()方法也是類似ApplicationShutdownHooks把鉤子新增到hooks陣列中並由runHooks方法呼叫,兩個類的操作很類似,但是一個(ApplicationShutdownHooks)是使用者級的鉤子一個(Shutdown)是系統級的,並且看上面ApplicationShutdownHooks呼叫Shutdown#add的索引引數為什麼是1,這個疑問可以在下方的11-15行得到解釋。另外的0和2索引通過System#registerShutdownHook分別在Console類和DeleteOnExitHook類使用。

/**
 * Package-private utility class containing data structures and logic
 * governing the virtual-machine shutdown sequence.
 *
 * @author   Mark Reinhold
 * @since    1.3
 */

class Shutdown {

    // 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];

    /**
     * Add a new shutdown hook.  Checks the shutdown state and the hook itself,
     * but does not do any security checks.
     *
     * The registerShutdownInProgress parameter should be false except
     * registering the DeleteOnExitHook since the first file may
     * be added to the delete on exit list by the application shutdown
     * hooks.
     *
     * @params slot  the slot in the shutdown hook array, whose element
     *               will be invoked in order during shutdown
     * @params registerShutdownInProgress true to allow the hook
     *               to be registered even if the shutdown is in progress.
     * @params hook  the hook to be registered
     *
     * @throw IllegalStateException
     *        if registerShutdownInProgress is false and shutdown is in progress; or
     *        if registerShutdownInProgress is true and the shutdown process
     *           already passes the given slot
     */
    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;
        }
    }

    /* 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;
                }
            }
        }
    }

}

4.Shutdown#sequence

Shutdown類中新增到hooks的鉤子在sequence方法中通過呼叫runHooks方法執行鉤子,sequence方法被exit()和shutdown()方法呼叫,shutdown方法由JNI(Java Native Interface)呼叫,exit()方法帶有一個status引數,用來判斷是否呼叫runAllFinalizers執行一些清理工作,正常退出程式都應該傳0,從Runtime#exit方法頭的註釋也可以佐證這一點。當status不為0的時候runAllFinalizers不會得到執行。

這裡有個疑問,從各方面資訊來看Shutdown#exit的status引數是否為0表示程式是否正常退出為什麼在Terminator#setup中訊號註冊處理器的時候,Shutdown#exit的status引數是非0?

/* The actual shutdown sequence is defined here.
 *
 * If it weren't for runFinalizersOnExit, this would be simple -- we'd just
 * run the hooks and then halt.  Instead we need to keep track of whether
 * we're running hooks or finalizers.  In the latter case a finalizer could
 * invoke exit(1) to cause immediate termination, while in the former case
 * any further invocations of exit(n), for any n, simply stall.  Note that
 * if on-exit finalizers are enabled they're run iff the shutdown is
 * initiated by an exit(0); they're never run on exit(n) for n != 0 or in
 * response to SIGINT, SIGTERM, etc.
 */
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();
}


/* 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);
    }
}


/* 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();
    }
}

5.Shutdown#runAllFinalizers

Shutdown#runAllFinalizers是個native方法,會呼叫Finalizer#runAllFinalizers,Finalizer#runAllFinalizers裡面會遍歷Finalizer連結串列變數unfinalized,逐個節點執行runFinalizer方法,根據方法體內的註釋

Clear stack slot containing this variable, to decrease the chances of false retention with a conservative GC

可以知道這是為了減少gc回收出錯機率的方法。

/* Invoked by java.lang.Shutdown */
static void runAllFinalizers() {
    if (!VM.isBooted()) {
        return;
    }

    forkSecondaryFinalizer(new Runnable() {
        private volatile boolean running;
        public void run() {
            if (running)
                return;
            final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
            running = true;
            for (;;) {
                Finalizer f;
                synchronized (lock) {
                    f = unfinalized;
                    if (f == null) break;
                    unfinalized = f.next;
                }
                f.runFinalizer(jla);
            }}});
}

private void runFinalizer(JavaLangAccess jla) {
    synchronized (this) {
        if (hasBeenFinalized()) return;
        remove();
    }
    try {
        Object finalizee = this.get();
        if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
            jla.invokeFinalize(finalizee);

            /* Clear stack slot containing this variable, to decrease
            /* the chances of false retention with a conservative GC 
            */
            finalizee = null;
        }
    } catch (Throwable x) { }
    super.clear();
}

以上內容梳理了鉤子的新增流程,以及由誰去呼叫鉤子。

下面梳理java程式註冊訊號監聽並呼叫Shutdown.exit的流程。

二、註冊訊號監聽

1.Terminator#setup

jvm對部分訊號(HUP/INT/TERM)的預設捕獲處理在Terminator類的setup()方法中
以下原始碼是jdk1.8.0_161的版本,我看了jdk1.8.0_144的setup方法是沒有對HUP訊號進行處理,不同版本略有差異

/* 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("HUP"), sh);
    } catch (IllegalArgumentException e) {
    }
    try {
        Signal.handle(new Signal("INT"), sh);
    } catch (IllegalArgumentException e) {
    }
    try {
        Signal.handle(new Signal("TERM"), sh);
    } catch (IllegalArgumentException e) {
    }
}

2.System#initializeSystemClass

Terminator類的setup()方法由System類初始化的時候在initializeSystemClass方法裡呼叫

/**
 * Initialize the system class.  Called after thread initialization.
 */
private static void initializeSystemClass() {

    // VM might invoke JNU_NewStringPlatform() to set those encoding
    // sensitive properties (user.home, user.name, boot.class.path, etc.)
    // during "props" initialization, in which it may need access, via
    // System.getProperty(), to the related system encoding property that
    // have been initialized (put into "props") at early stage of the
    // initialization. So make sure the "props" is available at the
    // very beginning of the initialization and all system properties to
    // be put into it directly.
    props = new Properties();
    initProperties(props);  // initialized by the VM

    // There are certain system configurations that may be controlled by
    // VM options such as the maximum amount of direct memory and
    // Integer cache size used to support the object identity semantics
    // of autoboxing.  Typically, the library will obtain these values
    // from the properties set by the VM.  If the properties are for
    // internal implementation use only, these properties should be
    // removed from the system properties.
    //
    // See java.lang.Integer.IntegerCache and the
    // sun.misc.VM.saveAndRemoveProperties method for example.
    //
    // Save a private copy of the system properties object that
    // can only be accessed by the internal implementation.  Remove
    // certain system properties that are not intended for public access.
    sun.misc.VM.saveAndRemoveProperties(props);


    lineSeparator = props.getProperty("line.separator");
    sun.misc.Version.init();

    FileInputStream fdIn = new FileInputStream(FileDescriptor.in);
    FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);
    FileOutputStream fdErr = new FileOutputStream(FileDescriptor.err);
    setIn0(new BufferedInputStream(fdIn));
    setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")));
    setErr0(newPrintStream(fdErr, props.getProperty("sun.stderr.encoding")));

    // Load the zip library now in order to keep java.util.zip.ZipFile
    // from trying to use itself to load this library later.
    loadLibrary("zip");

    // Setup Java signal handlers for HUP, TERM, and INT (where available).
    Terminator.setup();

    // Initialize any miscellenous operating system settings that need to be
    // set for the class libraries. Currently this is no-op everywhere except
    // for Windows where the process-wide error mode is set before the java.io
    // classes are used.
    sun.misc.VM.initializeOSEnvironment();

    // The main thread is not added to its thread group in the same
    // way as other threads; we must do it ourselves here.
    Thread current = Thread.currentThread();
    current.getThreadGroup().add(current);

    // register shared secrets
    setJavaLangAccess();

    // Subsystems that are invoked during initialization can invoke
    // sun.misc.VM.isBooted() in order to avoid doing things that should
    // wait until the application class loader has been set up.
    // IMPORTANT: Ensure that this remains the last initialization action!
    sun.misc.VM.booted();
}

System類的initializeSystemClass方法由虛擬機器呼叫
public final class System {

    /* register the natives via the static initializer.
     *
     * VM will invoke the initializeSystemClass method to complete
     * the initialization for this class separated from clinit.
     * Note that to use properties set by the VM, see the constraints
     * described in the initializeSystemClass method.
     */
    private static native void registerNatives();
    static {
        registerNatives();
    }

Terminator類的setup()方法做了訊號註冊,並且由虛擬機器通過呼叫System類的initializeSystemClass方法方式實現了對Terminator#setup()的呼叫完成訊號註冊。
訊號註冊流程呼叫鏈:VM->System#initializeSystemClass()->Terminator#setup()。