目录
理解停止 Java 进程的本质
应该如何正确地停止 Java 进程
如何注册关闭钩子
使用关闭钩子的注意事项
信号量机制
总结
理解停止 Java 进程的本质
我们知道, Java 程序的运行需要一个运行时环境, 即: JVM, 启动 Java 进程即启动了一个 JVM.
因此, 所谓停止 Java 进程, 本质上就是关闭 JVM.
那么, 哪些情况会导致 JVM 关闭呢?
应该如何正确地停止 Java 进程
通常来讲, 停止一个进程只需要杀死进程即可.
但是, 在某些情况下可能需要在 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 进程退出之前做一些善后的事情(实际上, 大多数时候都需要这样做).
如何注册关闭钩子
在 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://blog.csdn.net/u011001084/article/details/73480432 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 处理机制
来源: https://www.cnblogs.com/nuccch/p/10903162.html