jvm

JVM优雅退出

2019-07-28  本文已影响36人  相远相连

背景

在某个Java应用增加新功能,缩容机器,或者应用以及机器发生异常,通常会停止正在运行的应用,该应用通常正在运行着任务,如果停止应用的操作处理不当的话,很有可能会导致数据丢失,损坏,从而影响业务。所以在停止应用的时候,需要考虑如何安全优雅的退出。本文分成三部分:

  1. jvm关闭的几种情况
  2. 如何优雅关闭应用
  3. 几点注意事项

jvm关闭的几种情况

jvm通常有下面几种关闭的情况:

正常关闭
  1. 所有非daemon线程退出
  2. 调用System.exit()
  3. SIGINT(ctrl+c)
  4. SIGTERM(kill -15)

异常关闭
  1. 未捕获的异常
  2. oom

强制关闭
  1. SIGKILL(kill -9)
  2. 应用crash
  3. 机器宕机

对于正常关闭、异常关闭的几种情况,JVM关闭前,都会调用已注册的shutdown hooks
对于强制关闭的几种情况,会直接停止JVM进程,JVM不会调用已注册的shutdown hooks

其中有几点需要了解:

    public void registerShutdownHook() {
        if (this.shutdownHook == null) {
            // No shutdown hook registered yet.
            this.shutdownHook = new Thread() {
                @Override
                public void run() {
                    synchronized (startupShutdownMonitor) {
                        doClose();
                    }
                }
            };
            Runtime.getRuntime().addShutdownHook(this.shutdownHook);
        }
    }
  1. 关闭钩子本质上是一个线程,对于一个JVM中注册的多个关闭钩子会并发执行,所以JVM并不保证它们的执行顺序,建议在一个钩子中执行应用的关闭操作。

  2. 在关闭钩子中,不能执行注册、移除钩子的操作,JVM将关闭钩子序列初始化完毕后,不允许再次添加或者移除已经存在的钩子,否则JVM抛出 IllegalStateException。不能在钩子调用System.exit(),否则卡住JVM的关闭过程,但是可以调用Runtime.halt()

  3. jvm退出时会等待所有的钩子线程执行之后,再退出jvm,到时候会直接停止运行还在运行所有的线程(包括daemon非daemon)。

如何实现优雅关闭

通常一个应用会有两种场景:

  1. 外部驱动:接收外部的请求,转换成任务,并运行该任务。
  2. 内部驱动:自产自销,通常以一个定时任务的形式运行。

针对第一种场景,需要从两个方面考虑优雅关闭:

  1. 停止接收外部新的请求。
    对于rpc调用,需要在注册中心主动注销自身的服务,从而避免上游应用继续往该机器发送请求;对于MQ消费,需要主动告知MQ停止往该机器投递消息。

  2. 等待当前接收的所有任务执行完成
    通常会通过线程池的方式来实现,可以直接调用线程池的shutdown方法,注意需要了解shutdownNowshutdown以及awaitTermination方法的使用

针对第二种场景,定时任务通常有两种实现方式:

  1. 使用ScheduldExecutorService线程池方式,优雅退出直接参考上面的线程池关闭即可。

  2. 在单线程在里面while循环加sleep的方式实现,通常有两种优雅退出方式:

几点注意事项

public class Runtime {
    public void addShutdownHook(Thread hook) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("shutdownHooks"));
        }
        ApplicationShutdownHooks.add(hook);
    }
    ...
}

class ApplicationShutdownHooks {
    /* The set of registered hooks */
    private static IdentityHashMap<Thread, Thread> hooks;

    static synchronized void add(Thread hook) {
        hooks.put(hook, hook);
    }

    /* Remove a previously-registered hook.  Like the add method, this method
     * does not do any security checks.
     */
    static synchronized boolean remove(Thread hook) {
        return hooks.remove(hook) != 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) {
            while (true) {
                try {
                    hook.join();
                    break;
                } catch (InterruptedException ignored) {
                }
            }
        }
    }

Runtime.getRuntime().addShutdownHook本身会调用ApplicationShutdownHooks注册钩子,hooks维护了所有已经注册的钩子,由于jvm本身没有提供好用的方法去移除已经注册的钩子,可以通过反射的方式调用ApplicationShutdownHookshooks属性并清除该map里面的内容。

梵高的田野
上一篇下一篇

猜你喜欢

热点阅读