如何优雅地停止Java进程

Java 专栏收录该内容
141 篇文章 0 订阅


相关文章:
shutdownGracefully初识(一)
【Netty】shutdownGracefully初识(二) netty 关闭原理
如何优雅地停止Java进程 netty需要结合java语法,才能实现最合理的关闭netty

1. 理解停止Java进程的本质

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

通常来讲,停止一个进程只需要杀死进程即可。
但是,在某些情况下可能需要在JVM关闭之前执行一些数据保存或者资源释放的工作,此时就不能直接强制杀死Java进程。

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

综上所述:

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

2. 如何注册关闭钩子

在Java中注册关闭钩子通过Runtime类实现:

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

注意事项:
1.在这里执行的动作不能耗时太久
2.不能在这里再执行注册,移除关闭钩子的操作
3. 不能在这里调用System.exit()

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

2.1 正常关闭例子

我们看个完整的例子:

我们在主线程中注册一个钩子,当关闭系统时,会打印信息。在主线程中,我们新建一个子线程,子线程模拟一个耗时操作,当操作完成,就主动关闭系统。

public class ShutdownByCodeTest {

    public static void main(String[] args) {
        // 注册关闭钩子
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                System.out.println("do shutdown hook");
            }
        });

        Thread t = new Thread(() -> {
            try {
                Thread.sleep(2000); // 子线程休眠2s,模拟耗时操作
            } catch (Exception e) {

            }
            System.out.println("all things have been done .");
            //System.exit(0);   //先注释掉,后面需要放开,进行对比
        });
        t.start();

        try {
            Thread.sleep(15000); // 主线程休眠15s保证程序不立即结束
        } catch (Exception e) {

        }
        System.out.println("main thread start to stop");
    }

}

执行结果,15s后main线程执行结束,此时没有其他线程存活(更精确来说,是没有非守护线程存活),jvm即将关闭,进而触发钩子:

all things have been done .
main thread start to stop     '主线程即将结束,此时没有其他存活线程,会引发jvm关闭'
do shutdown hook          'jvm关闭事件触发之前注册的钩子'

我们放开System.exit(0); ,子线程会提前触发jvm关闭事件,再执行,2s后系统关闭,触发钩子:

all things have been done .
do shutdown hook     '子线程提前触发jvm关闭事件,进而触发回调函数'

2.2 异常关闭例子

前文的图一告诉我们2个场景的异常关闭例子,一个是oom,一个抛出异常,下面的代码中提供2个场景,通过mockWork中的注释来控制具体的场景,同时只能放开一处代码。

import java.util.ArrayList;
import java.util.List;

public class ShutdownTest {

    public static void main(String[] args) {

        System.out.println("Shutdown Test");

       
        // 注册关闭钩子
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                System.out.println("do shutdown hook");
            }
        });

        mockWork();

        System.out.println("Done.");

    }

    // 模拟进程正在运行
    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());
        }
    }

}

oom场景需要设置系统变量,即把内存区设置的非常小,否则不会产生oom

2.3 使用关闭钩子的注意事项

1.关闭钩子本质上是一个线程(也称为Hook线程),对于一个JVM中注册的多个关闭钩子它们将会并发执行,所以JVM并不保证它们的执行顺序;由于是并发执行的,那么很可能因为代码不当导致出现竞态条件或死锁等问题,为了避免该问题,强烈建议只注册一个钩子并在其中执行一系列操作。

某些场景下要提供at most once的保证,就是你的shutdownHook可能被调用多次,但其实关闭一次就够了,多次调用可能会引发一些意想不到的异常。比如KafkaStreamclose方法,就提供了这样的保证:

public synchronized boolean close(long timeout, TimeUnit timeUnit) {
        this.log.debug("Stopping Streams client with timeoutMillis = {} ms.", timeUnit.toMillis(timeout));
        if (!this.setState(KafkaStreams.State.PENDING_SHUTDOWN)) {
            this.log.info("Already in the pending shutdown state, wait to complete shutdown");
        } else {
       // ....

可以使用CAS操作来做这样的检查:

if (state.compareAndSet(ACTIVE, CLOSED)) {
   // close here
}

通过加synchronized ,保证执行时顺序的,通过CAS,保证只执行一次,并设置已执行状态,避免下次被触发时被重复执行。

2.shutdownHook的方法应该是线程安全的,用户可能多次发送信号导致方法被不同的线程被多次调用。

3.Hook线程会延迟JVM的关闭时间,这就要求在编写钩子过程中必须要尽可能的减少Hook线程的执行时间,避免hook线程中出现耗时的计算、等待用户I/O等等操作。

4.关闭钩子执行过程中可能被强制打断,比如在操作系统关机时,操作系统会等待进程停止,等待超时,进程仍未停止,操作系统会强制的杀死该进程,在这类情况下,关闭钩子在执行过程中被强制中止。

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

7.Hook线程中同样会抛出异常,对于未捕捉的异常,线程的默认异常处理器处理该异常(将异常信息打印到System.err),不会影响其他hook线程以及JVM正常退出。换句话说,shutdownHook调用过程中产生的所有异常都会被忽略掉并且可能不会输出任何提示信息

3. 信号量机制

注册关闭钩子的目的是为了在JVM关闭之前执行一些收尾的动作,而从上述描述可以知道,触发关闭钩子动作的执行需要满足JVM正常关闭或异常关闭的情形。

显然,我们应该正常关闭JVM(异常关闭JVM的情形不希望发生,也无法百分之百地完全杜绝),即执行:System.exit(),Ctrl + C, kill -15 进程ID等方式进行正常关闭。

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

信号具有平台相关性,不同平台下能使用的信号种类是有差异的。


Linux下支持的信号:
SEGV, ILL, FPE, BUS, SYS, CPU, FSZ, ABRT, INT, TERM, HUP, USR1, USR2, QUIT, BREAK, TRAP, PIPE


Windows下支持的信号:
SEGV, ILL, FPE, ABRT, INT, TERM, BREAK

实际上,大多数情况下的进程结束操作通常是在进程运行过程中需要停止进程或者重启进程,而不是等待进程自己运行结束(服务程序都是一直运行的,并不会主动结束)。

比如之前的那个完整例子,子线程主动停止的。并非外部驱动

也就是说,针对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");
    }
});

3.1完整信号量例子

主线程注册一个关闭事件的钩子,并且注册一个信号量监听器,信号量监听器负责接收信号量,如果是 'kill -15 '信号,发起一个关闭事件,进而钩子也随后被触发。

import java.util.ArrayList;
import java.util.List;

import sun.misc.Signal;
import sun.misc.SignalHandler;

public class ShutdownTest {

    public static void main(String[] args) {

        System.out.println("Shutdown Test");

        Signal sg = new Signal("TERM"); // 对应 linux上收到的 kill -15 pid信号量
        // 监听信号量
        Signal.handle(sg, new SignalHandler() {
            @Override
            public void handle(Signal signal) {
                if (signal.getName().equals("TERM")) {
                    System.out.println("signal handle: " + signal.getName());
                    System.exit(0);

                }
            }
        });
        // 注册关闭钩子
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                System.out.println("do shutdown hook");
            }
        });

        mockWork();

        System.out.println("Done.");

    }

    // 模拟进程正在运行,因为只有main一个进程,没有sleep代码就执行结束了
    //虽然有注册钩子等代码,但这些都没有新起线程
    private static void mockWork() {

        try {
            Thread.sleep(120 * 1000);   //main线程sleep 120s,保证系统一直运行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

在eclipse中sun.misc.Signal可能引用报错 ,可以参考 eclipse引用sun.misc开头的类

把上面编译的class文件全部拷贝至linux上,注意有子类
在这里插入图片描述
进入class文件所在目录,执行java ShutdownTest,另开一个控制台,输入ps -ef|grep ShutdownTest,找到进程pid,再执行kill -15 8971,其中8971为pid。

执行结果:

[root]# java ShutdownTest
Shutdown Test
signal handle: TERM
do shutdown hook

windows上面貌似找不到 kill -15 pid 命令,因此在linux可以演示

3.2 信号量注意事项

此时,需要注意信号量不能阻塞,否则会阻塞事件传递,参见 【netty进阶之路 跟着案例学netty】信号量停止netty

  • 1
    点赞
  • 0
    评论
  • 1
    收藏
  • 扫一扫,分享海报

参与评论 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值