1. 程式人生 > >如何優雅地停止Java程序

如何優雅地停止Java程序

目錄

  • 理解停止Java程序的本質
  • 應該如何正確地停止Java程序
    • 如何註冊關閉鉤子
    • 使用關閉鉤子的注意事項
    • 訊號量機制
  • 總結

理解停止Java程序的本質

我們知道,Java程式的執行需要一個執行時環境,即:JVM,啟動Java程序即啟動了一個JVM。
因此,所謂停止Java程序,本質上就是關閉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正常退出。

訊號量機制

註冊關閉鉤子的目的是為了在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程序(詳見: ,實際上這是有問題的。因為單純地監聽訊號量,並不能覆蓋到異常關閉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處理機