如何優雅的關閉JVM?
前言
1、基本概述
程序的啟動很簡單,啟動的時候通常會做一些預加載資源的操作。但是有時候關閉的時候,啟動的時候預加載的資源并沒有完全清理干凈,因此可以使用鉤子函數來完成。
2、JVM關閉的場景分類
直接看一張圖吧,本圖來自博客園的BarryWang,特在此說明。
從上面可以看到,JVM關閉主要分為了三類,第一種是正常的關閉,第二種是異常關閉的情況,第三種是強制關閉的情況。對于前兩種方式我們可以使用鉤子函數優雅的關閉,但是強制關閉的時候鉤子函數并不起作用。
有了這些概念,我們直接使用一個案例進行演示,再進行分析。
一、代碼演示鉤子函數
1、JVM正常關閉
直接看代碼吧,
- public class Test {
- public void start(){
- Runtime.getRuntime().addShutdownHook(new Thread(()->
- System.out.println("鉤子函數被執行,可以在這里關閉資源")
- ));
- }
- public static void main(String[] args) throws Exception{
- new Test().start();
- System.out.println("主應用程序在執行");
- }
- }
- //控制臺輸出
- //主應用程序在執行
- //鉤子函數被執行,可以在這里關閉資源
看控制臺打印,可以發現,主應用程序執行完之后就會調用鉤子函數,接下來就會正式的關閉JVM。
2、異常關閉
還是直接看代碼演示,這里我們演示異常關閉的第二種OOM的情況,我們可以先設置堆的大小為20M,然后在代碼中創建一個500M的對象,這樣就會OOM。參數是-Xmx20M;
- public class Test {
- public void start(){
- Runtime.getRuntime().addShutdownHook(new Thread(()->
- System.out.println("鉤子函數被執行,可以在這里關閉資源")
- ));
- }
- public static void main(String[] args) throws Exception{
- new Test().start();
- System.out.println("主應用程序在執行");
- Runtime.getRuntime().halt(1);
- byte[] b = new byte[500*1024*1024];
- }
- }
- //控制臺輸出
- //主應用程序在執行
- //鉤子函數被執行,可以在這里關閉資源
從控制臺可以看出,鉤子函數在異常關閉的時候依然會被調用。
3、強制關閉
這里我們使用Runtime.getRuntime().halt()來演示強勢關閉。這個方法和System.exit的區別是,System.exit會執行鉤子函數,但是Runtime.getRuntime().halt()不會。
- public class Test {
- public void start(){
- Runtime.getRuntime().addShutdownHook(new Thread(()->
- System.out.println("鉤子函數被執行,可以在這里關閉資源")
- ));
- }
- public static void main(String[] args) throws Exception{
- new Test().start();
- System.out.println("主應用程序在執行");
- Runtime.getRuntime().halt(1);
- }
- }
- //控制臺輸出
- //主應用程序在執行
從上面代碼的輸出可以看出,調用了Runtime.getRuntime().halt(1)就會強制關閉JVM,鉤子函數來不及執行就關閉了。而使用System.exit依然會執行。所以一般使用System.exit來關閉JVM。
4、移除鉤子函數
上面演示了鉤子函數的作用,有時候我們想移除也比較簡單。
- public class Test {
- public static void main(String[] args) throws Exception{
- //new Test().start();
- Thread willNotRun = new Thread(() ->
- System.out.println("Won't run!"));
- Runtime.getRuntime().addShutdownHook(willNotRun);
- System.out.println("主應用程序在執行");
- Runtime.getRuntime().removeShutdownHook(willNotRun);
- }
- }
- //控制臺輸出
- //主應用程序在執行
OK,鉤子函數的基本操作就寫到這,使用起來比較簡單,不過我之前看過Spring的啟動流程,所以又去那個啟動流程看了一波,發現也使用到了鉤子函數。
二、典型應用場景
1、Spring使用
Spring在關閉上下文的時候,可以使用鉤子函數來關閉殘留的資源。方法是使用ApplicationContext注冊一個鉤子函數即可。
- ApplicationContext.registerShutdownHook();
- //上面的這句代碼可以分析進去看看
- public void registerShutdownHook() {
- if (this.shutdownHook == null) {
- this.shutdownHook = new Thread() {
- @Override
- public void run() {
- //Spring正常關閉
- doClose();
- }
- };
- //調用鉤子函數關閉殘留資源
- Runtime.getRuntime().addShutdownHook(this.shutdownHook);
- }
- }
從源碼可以看出,Spring其實也是調用了Java的鉤子函數進行關閉的。
2、其他使用
我在很多博客中也看到了spark和hadoop的關閉,由于我沒看過源碼,所以這里我說一下結論,對于其他的使用場景,基本上也是調用了Java的鉤子函數來執行的。
結論
在關閉JVM的時候,我們可以封裝鉤子函數去優雅的關閉線程。不過在使用的時候還需要注意以下幾個方面:
1、鉤子函數本質是個線程
多個鉤子會并發執行,JVM并不保證它們的執行順序;因此最好是在一個鉤子中執行一系列操作。
2、鉤子中不能再新建鉤子
在關閉鉤子中,不能執行注冊、移除鉤子的操作,否則JVM拋出 IllegalStateException。也不能使用System.exit(),前面提到System.exit()會觸發鉤子函數的執行,但是Runtime.halt()可以,因為Runtime.halt()可以強制關閉。
3、鉤子里最好不要有耗時操作
鉤子函數主要用于關閉殘留資源,因此不要有一些耗時的操作。
OK,先寫到這。相關的經驗方法,并推薦幾本關于體驗、思考的書籍。希望對同學們有所啟發。
本文轉載自微信公眾號「愚公要移山」,可以通過以下二維碼關注。轉載本文請聯系愚公要移山公眾號。