咱們知道,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處理機制