Linux系统下如何优雅地关闭Java进程?

时间:2024-03-07 14:18:39

资料出处: http://www.sohu.com/a/329564560_700886

      https://www.cnblogs.com/nuccch/p/10903162.html

 

前言

  Linux系统下如何kill掉一个后台Java进程,相信童鞋们都知道如何操作。首先使用ps命令查找该Java进程的进程ID,然后使用kill命令进行杀掉。命令如下:

(1)ps查进程ID

[user@data2 ~]$ ps -ef | grep Test

user 2095020809 0 21:30 pts/1 00:00:00 java -jar Test.jar

user 21030 20996 0 21:30 pts/2 00:00:00 grep Test

(2)kill杀进程

[user@data2 ~]$ kill -9 20950

  再使用ps命令查该进程,发现进程Test.jar已经被杀掉。使用“kill -9 $pid”杀Java进程,干净利落。但该方法是不是结束Java后台进程的较好方法呢?

场景

思考下面的场景:

  “开发一个Java后台程序,其功能是不停地扫描Linux系统下的某个ftp目录。如果有文件,就经过数据转换写入到数据库中;如果没有文件,就sleep一秒钟。ftp目录下的文件不断地上传,Java程序处理完一个文件,就将该文件移到备份目录下面。”

  该场景涉及Java程序进行文件打开、文件读取、文件备份、数据库连接、数据库写入等操作。因为文件句柄和数据库连接在Linux系统中是有限的资源,所以文件和数据库操作完成,需要进行关闭。

  如果用户直接使用“kill -9”杀掉一个后台正在读取文件并写入数据库的Java进程。那么有可能文件和数据库连接没有正确关闭,而且数据文件也没有标识是否处理完成,或处理到哪个位置。

 

其他知识点介绍:

(1)Java System.exit() 退出程序

  在java 中退出程序,经常会使用System.exit(1) 或 System.exit(0),其中返回的status的值为0代表正常退出,非零代表异常退出。使用该方法可以在图形界面编程中实现程序的退出功能等。

  exit(int)方法终止当前正在运行的 Java 虚拟机,参数解释为状态码。根据惯例,非 0 的状态码表示异常终止。 而且,该方法永远不会正常返回。 这是唯一一个能够退出程序并不执行finally的情况。

(2)Linux kill -9 和 kill -15 的区别

  kill -9 PID 是操作系统从内核级别强制杀死一个进程.

  kill -15 PID 可以理解为操作系统发送一个通知告诉应用主动关闭.

  SIGNTERM(15) 的效果是正常退出进程,退出前可以被阻塞或回调处理。并且它是Linux缺省的程序中断信号。

 

  • kill -15 pid(默认)
  • 执行完该指令后,操作系统会发送一个 SIGTERM 信号给对应的程序。当程序接收到该信号后,可能会发生以下几种情况的一种:
  1. 当前程序立刻停止;
  2. 程序释放相应资源,然后再停止;
  3. 程序可能仍然继续运行。

  大部分程序会先释放自己的资源,然后再停止。但是也有程序可以在接受到信号量后,继续做其他一些事情,并且这些事情是可以配置的。如果程序正在等待IO,可能就不会立马做出响应。也就是说,15) SIGTERM 是可能被阻塞、被忽略的。

  • kill -9 pid
  • 如果 15) SIGTERM 可以不进行响应?那 9) SIGKILL就是必杀信号,多半 ROOT 会直接使用这个命令,但并不推荐这么做。

小结:在使用 kill -9 前,应该先使用 kill -15,给目标进程一个清理善后工作的机会。如果没有,可能会留下一些不完整的文件或状态,从而影响服务的再次启动。

 

 


理解停止Java进程的本质

我们知道,Java程序的运行需要一个运行时环境,即:JVM,启动Java进程即启动了一个JVM。
因此,所谓停止Java进程,本质上就是关闭JVM。
那么,哪些情况会导致JVM关闭呢?

 

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

通常来讲,停止一个进程只需要杀死进程即可。
但是,在某些情况下可能需要在JVM关闭之前执行一些数据保存或者资源释放的工作,此时就不能直接强制杀死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进程之后(如:在监听到操作系统信号量之后再注册关闭钩子也是可以的)。

使用关闭钩子的注意事项

  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 + C, kill -15 进程ID

  • System.exit():通常我们在程序运行完毕之后调用,这是在应用代码中写死的,无法在进程外部进行调用。
  • Ctrl + C:如果Java进程运行在操作系统前台,可以通过键盘中断的方式结束运行;但是当进程在后台运行时,就无法通过Ctrl + C方式退出了。
  • Kill (-15)SIGTERM信号:使用kill命令结束进程是使用操作系统的信号量机制,不论进程运行在操作系统前台还是后台,都可以通过kill命令结束进程,这也是结束进程使用得最多的方式。

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

第一步:在应用程序中监听信号量
由于不通的操作系统类型实现的信号量动作存在差异,所以监听的信号量需要根据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);
    }
});

第二步:注册关闭钩子

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())。