如何优雅地中止Java进程

目录

理解中止Java进程的本质

咱们知道,Java程序的运行须要一个运行时环境,即:JVM,启动Java进程即启动了一个JVM。
所以,所谓中止Java进程,本质上就是关闭JVM。
那么,哪些状况会致使JVM关闭呢?html

JVM关闭

应该如何正确地中止Java进程

一般来说,中止一个进程只须要杀死进程便可。
可是,在某些状况下可能须要在JVM关闭以前执行一些数据保存或者资源释放的工做,此时就不能直接强制杀死Java进程。java

  1. 对于正常关闭或异常关闭的几种状况,JVM关闭前,都会调用已注册的关闭钩子,基于这种机制,咱们能够将扫尾的工做放在关闭钩子中,进而使咱们的应用程序安全的退出。并且,基于平台通用性的考虑,更推荐应用程序使用System.exit(0)这种方式退出JVM。
  2. 对于强制关闭的几种状况:系统关机,操做系统会通知JVM进程等待关闭,一旦等待超时,系统会强制停止JVM进程;而kill -9Runtime.halt()断电系统crash这些方式会直接无商量停止JVM进程,JVM彻底没有执行扫尾工做的机会。

综上所述:安全

  1. 除非很是肯定不须要在Java进程退出以前执行收尾的工做,不然强烈不建议使用kill -9这种简单暴力的方式强制中止Java进程(除了系统关机系统Crash断电,和Runtime.halt()咱们无能为力以外)。
  2. 不论如何,都应该在Java进程中注册关闭钩子,尽最大可能地保证在Java进程退出以前作一些善后的事情(实际上,大多数时候都须要这样作)。

如何注册关闭钩子

在Java中注册关闭钩子经过Runtime类实现:并发

Runtime.getRuntime().addShutdownHook(new Thread(){
    @Override
    public void run() {
        // 在JVM关闭以前执行收尾工做
        // 注意事项:
        // 1.在这里执行的动做不能耗时过久
        // 2.不能在这里再执行注册,移除关闭钩子的操做
        // 3 不能在这里调用System.exit()
        System.out.println("do shutdown hook");
    }
});

为JVM注册关闭钩子的时机不固定,能够在启动Java进程以前,也能够在Java进程以后(如:在监听到操做系统信号量以后再注册关闭钩子也是能够的)。ide

使用关闭钩子的注意事项

1.关闭钩子本质上是一个线程(也称为Hook线程),对于一个JVM中注册的多个关闭钩子它们将会并发执行,因此JVM并不保证它们的执行顺序;因为是并发执行的,那么极可能由于代码不当致使出现竞态条件或死锁等问题,为了不该问题,强烈建议只注册一个钩子并在其中执行一系列操做。
2.Hook线程会延迟JVM的关闭时间,这就要求在编写钩子过程当中必需要尽量的减小Hook线程的执行时间,避免hook线程中出现耗时的计算、等待用户I/O等等操做。
3.关闭钩子执行过程当中可能被强制打断,好比在操做系统关机时,操做系统会等待进程中止,等待超时,进程仍未中止,操做系统会强制的杀死该进程,在这类状况下,关闭钩子在执行过程当中被强制停止。
4.在关闭钩子中,不能执行注册、移除钩子的操做,JVM将关闭钩子序列初始化完毕后,不容许再次添加或者移除已经存在的钩子,不然JVM抛出IllegalStateException异常。
5.不能在钩子调用System.exit(),不然卡住JVM的关闭过程,可是能够调用Runtime.halt()。
6.Hook线程中一样会抛出异常,对于未捕捉的异常,线程的默认异常处理器处理该异常(将异常信息打印到System.err),不会影响其余hook线程以及JVM正常退出。函数

信号量机制

优雅地关闭Java进程

注册关闭钩子的目的是为了在JVM关闭以前执行一些收尾的动做,而从上述描述能够知道,触发关闭钩子动做的执行须要知足JVM正常关闭或异常关闭的情形。
显然,咱们应该正常关闭JVM(异常关闭JVM的情形不但愿发生,也没法百分之百地彻底杜绝),即执行:System.exit()Ctrl + Ckill -15 进程ID操作系统

  • System.exit():一般咱们在程序运行完毕以后调用,这是在应用代码中写死的,没法在进程外部进行调用。
  • Ctrl + C:若是Java进程运行在操做系统前台,能够经过键盘中断的方式结束运行;可是当进程在后台运行时,就没法经过Ctrl + C方式退出了。
  • Kill (-15)SIGTERM信号:使用kill命令结束进程是使用操做系统的信号量机制,不论进程运行在操做系统前台仍是后台,均可以经过kill命令结束进程,这也是结束进程使用得最多的方式。

实际上,大多数状况下的进程结束操做一般是在进程运行过程当中须要中止进程或者重启进程,而不是等待进程本身运行结束(服务程序都是一直运行的,并不会主动结束)。也就是说,针对JVM正常关闭的情形,大多数状况是使用kill -15 进程ID的方式实现的。那么,咱们是否能够结合操做系统的信号量机制和JVM的关闭钩子实现优雅地关闭Java进程呢?答案是确定的,具体实现步骤以下:.net

第一步:在应用程序中监听信号量
因为不通的操做系统类型实现的信号量动做存在差别,因此监听的信号量须要根据Java进程实际运行的环境而定(如:Windows使用SIGINT,Linux使用SIGTERM)。线程

Signal sg = new Signal("TERM"); // kill -15 pid
Signal.handle(sg, new SignalHandler() {
    @Override
    public void handle(Signal signal) {
        System.out.println("signal handle: " + signal.getName());
        // 监听信号量,经过System.exit(0)正常关闭JVM,触发关闭钩子执行收尾工做
        System.exit(0);
    }
});

第二步:注册关闭钩子code

Runtime.getRuntime().addShutdownHook(new Thread(){
    @Override
    public void run() {
        // 执行进程退出前的工做
        // 注意事项:
        // 1.在这里执行的动做不能耗时过久
        // 2.不能在这里再执行注册,移除关闭钩子的操做
        // 3 不能在这里调用System.exit()
        System.out.println("do something");
    }
});

完整示例以下:

public class ShutdownTest {
    public static void main(String[] args) {
        System.out.println("Shutdown Test");

        Signal sg = new Signal("TERM"); // kill -15 pid
        // 监听信号量
        Signal.handle(sg, new SignalHandler() {
            @Override
            public void handle(Signal signal) {
                System.out.println("signal handle: " + signal.getName());
                System.exit(0);
            }
        });
        // 注册关闭钩子
        Runtime.getRuntime().addShutdownHook(new Thread(){
            @Override
            public void run() {
                // 在关闭钩子中执行收尾工做
                // 注意事项:
                // 1.在这里执行的动做不能耗时过久
                // 2.不能在这里再执行注册,移除关闭钩子的操做
                // 3 不能在这里调用System.exit()
                System.out.println("do shutdown hook");
            }
        });

        mockWork();

        System.out.println("Done.");
        System.exit(0);
    }

    // 模拟进程正在运行
    private static void mockWork() {
        //mockRuntimeException();
        //mockOOM();
        try {
            Thread.sleep(120 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } 
    }

    // 模拟在应用中抛出RuntimeException时会调用注册钩子
    private static void mockRuntimeException() {
        throw new RuntimeException("This is a mock runtime ex");
    }

    // 模拟应用运行出现OOM时会调用注册钩子
    // -xms10m -xmx10m
    private static void mockOOM() {
        List list = new ArrayList();
        for(int i = 0; i < 1000000; i++) {
            list.add(new Object());
        }
    }
}

总结

网上有文章总结说能够直接使用监听信号量的机制来实现优雅地关闭Java进程(详见:Java程序优雅关闭的两种方法),实际上这是有问题的。由于单纯地监听信号量,并不能覆盖到异常关闭JVM的情形(如:RuntimeException或OOM),这种方式与注册关闭钩子的区别在于:
1.关闭钩子是在独立线程中运行的,当应用进程被kill的时候main函数就已经结束了,仅会运行ShutdownHook线程中run()方法的代码。
2.监听信号量方法中handle函数会在进程被kill时收到TERM信号,但对main函数的运行不会有任何影响,须要使用别的方式结束main函数(如:在main函数中添加布尔类型的flag,当收到TERM信号时修改该flag,程序便会正常结束;或者在handle函数中调用System.exit())。

【参考】
http://www.javashuo.com/article/p-pbcsrivc-gu.html JVM安全退出(如何优雅的关闭java服务)
http://yuanke52014.iteye.com/blog/2306805 Java保证程序结束时调用释放资源函数
https://tessykandy.iteye.com/blog/2005767 基于kill信号优雅的关闭JAVA程序
https://www.cnblogs.com/taobataoma/archive/2007/08/30/875743.html Linux 信号signal处理机制

相关文章
相关标签/搜索