JVM鉤子函式的使用

朱又燉粉條發表於2021-08-21

一、問題引入

背景

在編寫一個需要持續在後臺執行的程式的時候遇到了這樣的場景:我的程式在主函式中建立了一個執行緒池週期性地執行任務,我希望主執行緒和執行緒池都持續執行,但如果收到外部的關閉訊號時,主執行緒和執行緒池也都能同時退出。想到的就是程式結束的時候需要有一個stop()方法去手動關閉執行緒池,但是怎麼控制這個stop()方法在我想要的時候呼叫,以什麼形式去接收外部的關閉訊號也成了需要考慮的問題。

原始思路

最開始的嘗試是我將程式的執行和停止分別用"start"和"stop"兩種狀態表示,然後用一個狀態檔案state去記錄當前的狀態(程式啟動時預設是"start"),如果想要關閉這個正在執行的程式,就去修改狀態檔案state,將裡面內容變為"stop"。同時在主函式中開啟這個狀態檔案,迴圈監聽裡面的內容,如果發現變為"stop",就去呼叫stop()方法執行關閉邏輯。按照這個思路,我寫了一個簡單的程式在IDEA中測試了一下效果,發現是可行的。但是當我將程式打包,在mac系統上執行jar包進行測試的時候,不知什麼原因,程式總是讀到state檔案剛開啟時的內容,不能檢測到state檔案的變化,無法按我設想的方式進行關閉。因此只能另想辦法。

無意間看見JVM鉤子函式的介紹,發現這可能正是我想要的,於是趕緊拿來試一試。

二、JVM鉤子使用場景

JVM關閉的情況如下圖所示分為三類,第一種是正常的關閉,第二種是異常關閉的情況,第三種是強制關閉的情況。

JVM鉤子函式對於前兩種方式都可以進行優雅的關閉,但是對最後一種強制關閉就不起作用了。

下面我會根據這三種JVM關閉過程進行簡單演示。

正常關閉

程式碼如下:

public class TestJVMHook {
    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(()->
                stop()
        ));
        start();
        System.out.println("===程式正常結束===");
    }
    public static void start() {
        System.out.println("===呼叫start()方法===");
    }

    public static void stop() {
        System.out.println("===呼叫stop()方法===");
    }
}

執行結果:

===呼叫start()方法===
===程式正常結束===
===呼叫stop()方法===

可以看到,在鉤子函式中宣告瞭stop()方法,然後程式正常結束後會自動呼叫鉤子函式。

異常關閉

異常關閉分為OOM和RuntimeException兩種情況,我用除數為0的執行時異常來演示。

程式碼如下:

public class TestJVMHook {
    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(()->
                stop()
        ));
        start();
        int res = 10/0;
        System.out.println("===程式結束===");
    }
    public static void start() {
        System.out.println("===呼叫start()方法===");
    }

    public static void stop() {
        System.out.println("===呼叫stop()方法===");
    }
}

執行結果:

===呼叫start()方法===
===呼叫stop()方法===
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at com.example.TestJVMHook.main(TestJVMHook.java:9)

可以看到執行"10/0"時發生執行時異常,並不會正常列印下一行語句,但仍然會自動呼叫鉤子函式中stop()方法。

強制關閉

這裡我們啟動一個迴圈程式,然後手動去關閉它。

程式碼如下:

public class TestJVMHook {
    public static void main(String[] args) throws InterruptedException {
        Runtime.getRuntime().addShutdownHook(new Thread(()->
                stop()
        ));
        int count = 1;
        start();
        while(true){
            System.out.println("迴圈計數器"+(count++));
            Thread.sleep(5*1000);
        }
    }
    public static void start() {
        System.out.println("===呼叫start()方法===");
    }

    public static void stop() {
        System.out.println("===呼叫stop()方法===");
    }
}

啟動後檢視程式id,然後通過"kill -9 <pid>"強制關閉:

執行結果:

還是上面那段程式碼,再次啟動,採用"kill <pid>"關閉:

發現通過"kill "正常關閉可以有效呼叫鉤子函式,但是"kill -9 "強制關閉則不會呼叫鉤子函式。

三、迴歸問題

經過一系列測試,驗證了JVM鉤子函式確實可以實現我想要的資源關閉效果。由於我的程式是一個迴圈程式,需要手動關閉,因此可以在關閉程式的指令碼中通過kill 的方式進行鉤子函式的呼叫。

相關文章