如何優雅地停止Java進程


目錄

理解停止Java進程的本質

我們知道,Java程序的運行需要一個運行時環境,即:JVM,啟動Java進程即啟動了一個JVM。
因此,所謂停止Java進程,本質上就是關閉JVM。
那么,哪些情況會導致JVM關閉呢?

JVM關閉

應該如何正確地停止Java進程

通常來講,停止一個進程只需要殺死進程即可。
但是,在某些情況下可能需要在JVM關閉之前執行一些數據保存或者資源釋放的工作,此時就不能直接強制殺死Java進程。

  1. 對於正常關閉或異常關閉的幾種情況,JVM關閉前,都會調用已注冊的關閉鈎子,基於這種機制,我們可以將掃尾的工作放在關閉鈎子中,進而使我們的應用程序安全的退出。而且,基於平台通用性的考慮,更推薦應用程序使用System.exit(0)這種方式退出JVM。
  2. 對於強制關閉的幾種情況:系統關機,操作系統會通知JVM進程等待關閉,一旦等待超時,系統會強制中止JVM進程;而kill -9Runtime.halt()斷電系統crash這些方式會直接無商量中止JVM進程,JVM完全沒有執行掃尾工作的機會。

綜上所述:

  1. 除非非常確定不需要在Java進程退出之前執行收尾的工作,否則強烈不建議使用kill -9這種簡單暴力的方式強制停止Java進程(除了系統關機系統Crash斷電,和Runtime.halt()我們無能為力之外)。
  2. 不論如何,都應該在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正常退出。

信號量機制

優雅地關閉Java進程

注冊關閉鈎子的目的是為了在JVM關閉之前執行一些收尾的動作,而從上述描述可以知道,觸發關閉鈎子動作的執行需要滿足JVM正常關閉或異常關閉的情形。
顯然,我們應該正常關閉JVM(異常關閉JVM的情形不希望發生,也無法百分之百地完全杜絕),即執行:System.exit()Ctrl + Ckill -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處理機制


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM