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 [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 信号给对应的程序。当程序接收到该信号后,可能会发生以下几种情况的一种:
- 当前程序立刻停止;
- 程序释放相应资源,然后再停止;
- 程序可能仍然继续运行。
大部分程序会先释放自己的资源,然后再停止。但是也有程序可以在接受到信号量后,继续做其他一些事情,并且这些事情是可以配置的。如果程序正在等待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进程。
- 对于正常关闭或异常关闭的几种情况,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())。
Linux系统下如何优雅地关闭Java进程?的更多相关文章
- 如何优雅地停止Java进程
目录 理解停止Java进程的本质 应该如何正确地停止Java进程 如何注册关闭钩子 使用关闭钩子的注意事项 信号量机制 总结 理解停止Java进程的本质 我们知道,Java程序的运行需要一个运行时环境 ...
- linux系统下运行java项目的脚本编写
本文主要讲linux系统下运行jar包,至于如何打包jar包,放到linux系统下可以参考其他的博客. 在linux系统下运行jar包的命令如下: 1.java -jar xxxxx.jar // ...
- linux 系统下java开发环境的配置
在安装之前,确保你的linux系统下有 jdk,jboss等相关软件 一.配置JDK环境变量 步骤: 解压缩JDK文件: unzip jdk1.6.0_31.zip 目录下显示文件夹jdk1.6.0_ ...
- Linux系统下的shutdown命令用于安全的关闭/重启计算机
Linux系统下的shutdown命令用于安全的关闭/重启计算机,它不仅可以方便的实现定时关机,还可以由用户决定关机时的相关参数.在执行shutdown命令时,系统会给每个终端(用户)发送一条屏显,提 ...
- Linux系统下Java 转换Word到PDF时,结果文档内容乱码的解决方法
本文分享在Linux系统下,通过Java 程序代码将Word转为PDF文档时,结果文档内容出现乱码该如何解决.具体可参考如下内容: 1.问题出现的背景 在Windows系统中,使用Spire.Doc ...
- Linux系统下重启Tomcat
在Linux系统下,重启Tomcat使用命令操作的! 首先,进入Tomcat下的bin目录 cd /usr/local/tomcat/bin 使用Tomcat关闭命令 ./shutdown.sh 查看 ...
- Linux系统下,启动Tomcat有时报Address already in use
一.Linux系统下,启动Tomcat有时报Address already in use<null>... 1.那是因为你的tomcat已经启动了tomcat自带的关闭脚本,有时候关闭看上 ...
- linux系统下的权限知识梳理
下面对linux系统下的有关权限操作命令进行了梳理总结,并配合简单实例进行说明.linux中除了常见的读(r).写(w).执行(x)权限以外,还有其他的一些特殊或隐藏权限,熟练掌握这些权限知识的使用, ...
- linux系统下修改文件夹目录权限
linux系统下修改文件夹目录权限 文件夹权限问题 Linux.Fedora.Ubuntu修改文件.文件夹权限的方法差不多.很多人开始接触Linux时都很头痛Linux的文件权限问题.这里告诉大家如何 ...
随机推荐
- Tkinter 之ScrollBar滚动条标签
一.参数说明 参数 作用 background (bg) 设置背景颜色 borderwidth (bd) 指定边框宽度,通常是 2 像素 cursor 指定当鼠标在上方飘过的时候的鼠标样式 orie ...
- エンジニア死滅シタ世界之学べない学校 [MISSION LEVEL: C]-Python3
答案 # coding: utf-8 # 自分の得意な言語で # Let's チャレンジ!! N=input() w_a=0 w_b=0 gpc_dict={ "gg":0,&qu ...
- 2.linux的增删改查
一.增删改查 1.建立文件和目录 mkdir /tmp/xueying 2.cd 进入的路径 绝对路径:以根目录为其实目录的路径 ...
- java之map遍历
java开发中常常会用到遍历,所以下边就列举四种map的遍历方法. public class testMap { public static void main(String[] args) { Ma ...
- Celery如何修复Python的GIL问题
小结: 1. Celery如何修复Python的GIL问题https://python.freelycode.com/contribution/detail/346 最近,我重读了Glyph写的Uny ...
- Ionic4.x 中的 UI 组件(UI Components) 日期组件
1.日期组件的基本使用 官方文档:https://ionicframework.com/docs/api/datetime 模板中: <ion-datetime display-format=& ...
- paint之文字示例
package com.loaderman.customviewdemo; import android.content.Context; import android.graphics.Canvas ...
- JS时间转换,url编码,jquery返回类型等问题
1.当时间被转换为json格式后会被转换成 /Date(...)/ 这种格式,其中...为时间转换成妙后的一串整数 function changeDateFormat(cellval) { )); v ...
- 页面被iframe与无刷新更换url方法
页面被iframe问题解决方法 if (window.top.location !== window.self.location) { const data = JSON.stringify({ if ...
- var和val的区别
var是一个可变变量,这是一个可以通过重新分配来更改为另一个值的变量.这种声明变量的方式和java中声明变量的方式一样. val是一个只读变量,这种声明变量的方式相当于java中的final变量.一个 ...