咱们知道,Java程序的运行须要一个运行时环境,即:JVM,启动Java进程即启动了一个JVM。
所以,所谓中止Java进程,本质上就是关闭JVM。
那么,哪些状况会致使JVM关闭呢?html
一般来说,中止一个进程只须要杀死进程便可。
可是,在某些状况下可能须要在JVM关闭以前执行一些数据保存或者资源释放的工做,此时就不能直接强制杀死Java进程。java
系统关机
,操做系统会通知JVM进程等待关闭,一旦等待超时,系统会强制停止JVM进程;而kill -9
、Runtime.halt()
、断电
、系统crash
这些方式会直接无商量停止JVM进程,JVM彻底没有执行扫尾工做的机会。综上所述:安全
kill -9
这种简单暴力的方式强制中止Java进程(除了系统关机
,系统Crash
,断电
,和Runtime.halt()
咱们无能为力以外)。在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进程以后(如:在监听到操做系统信号量以后再注册关闭钩子也是能够的)。ide
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
。操作系统
Ctrl + C
方式退出了。实际上,大多数状况下的进程结束操做一般是在进程运行过程当中须要中止进程或者重启进程,而不是等待进程本身运行结束(服务程序都是一直运行的,并不会主动结束)。也就是说,针对JVM正常关闭的情形,大多数状况是使用kill -15 进程ID
的方式实现的。那么,咱们是否能够结合操做系统的信号量机制和JVM的关闭钩子实现优雅地关闭Java进程呢?答案是确定的,具体实现步骤以下:.net
第一步:在应用程序中监听信号量
因为不通的操做系统类型实现的信号量动做存在差别,因此监听的信号量须要根据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); } });
第二步:注册关闭钩子code
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())。
【参考】
http://www.javashuo.com/article/p-pbcsrivc-gu.html 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处理机制