jvm关闭

。_饼干妹妹 提交于 2020-02-05 13:50:12

关闭方式

正常关闭

  • 最后一个普通线程(非守护线程)结束
  • 调用了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方法把其他线程一并杀掉

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!