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

资料出处: 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

[[email protected] ~]$ 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杀进程

[[email protected] ~]$ 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正常退出。

信号量机制

注册关闭钩子的目的是为了在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())。

原文地址:https://www.cnblogs.com/myseries/p/12078604.html

时间: 2024-10-14 16:44:14

Linux系统下如何优雅地关闭Java进程?的相关文章

linux系统下部署TOMCAT异常:java.net.UnknownHostException

原文出自:http://www.myexception.cn/operating-system/444024.html linux系统下部署TOMCAT错误:java.net.UnknownHostException今天在修改linux环境中 /etc/hosts文件时候,保存之后,访问页面的时候突然这个错,后来网上查了下,解决了java.net.UnknownHostException: vps**: vps**        at java.net.InetAddress.getLocalH

linux系统下运行java项目的脚本编写

本文主要讲linux系统下运行jar包,至于如何打包jar包,放到linux系统下可以参考其他的博客. 在linux系统下运行jar包的命令如下: 1.java -jar xxxxx.jar  // 当前ssh窗口被锁定,可按CTRL + C打断程序运行,或直接关闭窗口,程序退出 2.java -jar xxxxx.jar &   //当前ssh窗口不被锁定,但是当窗口关闭时,程序中止运行. 3.nohup Java -jar xxxxxx.jar &  //意思是不挂断运行命令,当账户退

Linux系统下的shutdown命令用于安全的关闭/重启计算机

Linux系统下的shutdown命令用于安全的关闭/重启计算机,它不仅可以方便的实现定时关机,还可以由用户决定关机时的相关参数.在执行shutdown命令时,系统会给每个终端(用户)发送一条屏显,提示关机操作.定时关机只需要一个简单的参数,既可以是倒计时,也可以是确切的时间. 命令格式 1 shutdown [选项] [时间] [消息] 并有如下选项: - k 不执行任何关机操作,只发出警告信息给所有用户 - r 重新启动计算机 - h 关机并彻底断电 - f 快速关机且重启动时跳过fsck

LINUX系统下Java和Scala的环境配置

LINUX系统下Java和Scala的环境配置 最近,笔者在研究一个有关“自然语言处理”的项目,在这个项目中,需要我们用Spark进行编程.而Spark内核是由Scala语言开发的,所以在使用Spark之前,我们必须配置好Scala,而Scala又是运行在JVM上的,所以在配置Scala之前,先要配置好JDK.下面是我个人的一些总结. 我是在Win7系统下开的虚拟机,虚拟机的系统是CentOS7,在创建虚拟机完成后,它会自带一个OpenJDK,我的版本是这样的: 但是因为最开始不了解这个情况,所

Linux系统下Jsp环境部署

-------本文大纲 简介 Jsp环境部署 Tomcat目录结构 SHOP++网上商城系统安装 --------------- 一.简介 JSP JSP(Java Server Pages)是由Sun Microsystems公司倡导.许多公司参与一起建立的一种动态网页技术标准.在传统的网页HTML文件(*.htm,*.html)中插入Java程序段(Scriptlet)和JSP标记(tag),从而形成JSP文件(*.jsp).简单地说,jsp就是可能包含了java程序段的html文件(由ja

Linux系统下安装jdk1.8

JDK安装分为两种方式  一种是解压tar.gz配置安装, 一种是rpm安装,我这里是tar.gz安装方式 一.首先在oracle官方网下载jdk,网址如下:http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 二.将jdk-8u65-linux-i586.gz拷贝到Linux系统下 三.在/usr/local下建立java目录 mkdir java 四.将jdk-8u65-linu

Linux系统下如何查看物理内存占用率

Linux系统下如何查看物理内存占用率 Linux下看内存和CPU使用率一般都用top命令,但是实际在用的时候,用top查看出来的内存占用率都非常高,如:Mem:   4086496k total, 4034428k used,    52068k free,   112620k buffersSwap: 4192956k total,   799952k used, 3393004k free, 1831700k cached 接近98.7%,而实际上的应用程序占用的内存往往并没这么多, PI

Linux系统下修改环境变量PATH路径的三种方法

比如要把/etc/apache/bin目录添加到PATH中,方法有三: 1.#PATH=$PATH:/etc/apache/bin 使用这种方法,只对当前会话有效,也就是说每当登出或注销系统以后,PATH 设置就会失效 2.#vi /etc/profile 在适当位置添加 PATH=$PATH:/etc/apache/bin (注意:= 即等号两边不能有任何空格) 这种方法最好,除非你手动强制修改PATH的值,否则将不会被改变 3.#vi ~/.bash_profile 修改PATH行,把/et

linux系统下修改文件夹目录权限

linux系统下修改文件夹目录权限 文件夹权限问题 Linux.Fedora.Ubuntu修改文件.文件夹权限的方法差不多.很多人开始接触Linux时都很头痛Linux的文件权限问题.这里告诉大家如何修改Linux文件-文件夹权限.以主文件夹下的一个名为cc的文件夹为例. 下面一步一步介绍如何修改权限: 1.打开终端.输入su(没 Linux.Fedora.Ubuntu修改文件.文件夹权限的方法差不多.很多人开始接触Linux时都很头痛Linux的文件权限问题.这里告诉大家如何修改Linux文件