JVM优雅退出

背景

在某个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

其中有几点需要了解:

  • linux信号以及处理
    其中ctrl+c,kill -15,kill -9 都是通过发送信号的方式来通知jvm进行关闭操作。

  • daemon线程和非daemon线程区别
    daemon线程在jvm里面的定义是: 如果jvm中只有daemon线程在运行,则jvm退出。通常默认新起的线程都是非daemon线程,可以通过设置线程的属性,将线程设置为daemon线程。

  • jvm shutdown hooks
    jvm提供了Runtime.getRuntime().addShutdownHook方法用来注册自定义的关闭逻辑,如下所示是spring框架注册的关闭逻辑

    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的方式实现,通常有两种优雅退出方式:

  • 使用volatile类型的共享变量作为退出标志
    通常在使用一个变量标识作为while循环判断的依据,该变量启动之后值为true,在自定义的jvm关闭钩子里面,修改该变量的值为false,为false时停止退出循环,结束线程。
  • 通过Thread.interrupt方法
    在jvm关闭钩子里面,获取到需要关闭的线程对象,调用thread.interrupt方法,如果此线程处于阻塞状态(比如调用了sleep方法,wait方法,或者io等待),则会立马退出阻塞,并抛出InterruptedException异常;如果此线程正处于运行之中,则线程不受任何影响,继续运行,仅仅是置线程的状态为中断状态interrupted
    在编写线程相关的代码时,需要捕获InterruptedException异常(捕获到异常时会清除interrupted状态),并且在合理的位置调用 isInterrupted方法来判断查看自己是否被中断,并做退出操作。

几点注意事项

  • 现在一般的应用都是基于spring框架开发的,spring框架本身会注册shutdown hook,关闭的时候会根据bean的依赖关系按序执行bean的destory逻辑,先destory被依赖的bean。呈一个树状的destory顺序。在编写bean实例相关代码的时候,最好加上destory方法,并做退出操作。

  • 注册jvm钩子的时候,最好设置线程名,并打印关键日志,以便排除问题。

  • 在所有的shutdown hook执行结束之后,会直接停止jvm,这一动作会停止还在运行的所有的线程(包括daemon和非daemon),所以最好在hook的代码里面增加需要关闭线程的状态判断,保证线程先关闭,再结束hook的运行。

  • 因为jvm钩子是异步并发执行的,可能会乱序,所以可以强制指定jvm只执行自己设置的钩子,去除其他引入的JAR包里面注册的钩子。jvm钩子的源码如下:

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里面的内容。

  • 需要考虑接受到关闭信号之后,jvm长时间没有关闭的情况,避免应用关闭失败。通常设置最大等待时间,最大等待时间之后,直接强制关闭,比如执行kill -9
梵高的田野
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,440评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,814评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,427评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,710评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,625评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,511评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,162评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,311评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,262评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,278评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,989评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,583评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,664评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,904评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,274评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,856评论 2 339

推荐阅读更多精彩内容