聊一聊未捕獲異常與進程退出的關聯

  • 2020 年 2 月 10 日
  • 筆記

之前的文章JVM 如何處理未捕獲異常 我們介紹了JVM如何處理未捕獲異常,今天我們研究一個更加有意思的問題,就是在JVM中如果發生了未捕獲異常,會導致JVM進程退出么。

關於什麼是未捕獲異常,我們在之前的文章已經介紹過,這裡不再贅述,如欲了解,請閱讀JVM 如何處理未捕獲異常

輔助方法

一個產生未捕獲異常的方法

//In Utils.java file        public static void causeNPE() {            String s = null;            s.length();        }  

執行緒睡眠方法

//In Utils.java file        public static void makeThreadSleep(long durationInMillSeconds) {            try {                Thread.sleep(durationInMillSeconds);            } catch (InterruptedException e) {                System.out.println("makeThreadSleep interrupted");                e.printStackTrace();            }        }  

使用該方法的目的主要有

  • 讓當前執行緒睡眠,確保其他執行緒啟動完成
  • 讓當前執行緒睡眠,確保當前執行緒不至於快速結束而銷毀

列印全部執行緒資訊方法

//In Utils.java file        public static void dumpAllThreadsInfo() {            Set<Thread> threadSet = Thread.getAllStackTraces().keySet();            for(Thread thread: threadSet) {                System.out.println("dumpAllThreadsInfo thread.name=" + thread.getName()                        + ";thread.state=" + thread.getState()                        + ";thread.isAlive=" + thread.isAlive()                        + ";group=" + thread.getThreadGroup()                );            }        }  

列印輔助測試的時間

//輸出結果類似 16:55:55        public static String getTimeForDebug() {            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");            return sdf.format(new Date());        }  

驗證方法

這裡的驗證我們按照表現來區分,我們將驗證以下場景

  • 在子執行緒中製造未捕獲異常
  • 在主執行緒中製造未捕獲異常

同時上面的場景,在通用的JVM和Android上表現有一些差異,我們也都會進行覆蓋研究。

子執行緒中的未捕獲異常

我們使用下面的程式碼,模擬一個在子執行緒中出現未捕獲異常的場景。

private static void startErrorThread() {        new Thread(new Runnable(){                @Override            public void run() {                System.out.println("startErrorThread currentThread.name=" + Thread.currentThread().getName()                + "; happened at " + Utils.getTimeForDebug());                Utils.causeNPE();            }        }).start();        Utils.makeThreadSleep(10 * 1000);        System.out.println("Thread main sleepFinished at " + Utils.getTimeForDebug());        Utils.dumpAllThreadsInfo();    }  

我們期待的輸出結果是

  • 新啟動的子線(應該是Thread-0)程因為NPE未捕獲而導致執行緒銷毀
  • 主執行緒不受剛剛異常的影響(進程還存在),在睡眠10秒後,會列印出所有執行緒的資訊(不包含剛剛崩潰執行緒Thread-0的資訊)
//異常發生 輸出執行緒名稱和發生異常的時間    startErrorThread currentThread.name=Thread-0; happened at 16:59:04    //異常崩潰的資訊    Exception in thread "Thread-0" java.lang.NullPointerException      at Utils.causeNPE(Utils.java:35)      at Main$3.run(Main.java:115)      at java.lang.Thread.run(Thread.java:748)    //主執行緒睡眠結束(對比時間,確定差為10秒)    Thread main sleepFinished at 16:59:14    //主執行緒不受影響,繼續執行操作    dumpAllThreadsInfo thread.name=Attach Listener;thread.state=RUNNABLE;thread.isAlive=true;group=java.lang.ThreadGroup[name=system,maxpri=10]    dumpAllThreadsInfo thread.name=Reference Handler;thread.state=WAITING;thread.isAlive=true;group=java.lang.ThreadGroup[name=system,maxpri=10]    dumpAllThreadsInfo thread.name=Monitor Ctrl-Break;thread.state=RUNNABLE;thread.isAlive=true;group=java.lang.ThreadGroup[name=main,maxpri=10]    dumpAllThreadsInfo thread.name=Signal Dispatcher;thread.state=RUNNABLE;thread.isAlive=true;group=java.lang.ThreadGroup[name=system,maxpri=10]    dumpAllThreadsInfo thread.name=Finalizer;thread.state=WAITING;thread.isAlive=true;group=java.lang.ThreadGroup[name=system,maxpri=10]    dumpAllThreadsInfo thread.name=main;thread.state=RUNNABLE;thread.isAlive=true;group=java.lang.ThreadGroup[name=main,maxpri=10]    //進程結束    Process finished with exit code 0  

看起來,子執行緒發生未捕獲的異常不會導致進程的退出(也不會影響其他的執行緒)。

Android有點不一樣

這個時候可能做Android開發的同學可能會站起來。

提問:不對啊,我把你的程式碼放到Android項目中執行,會出現應用已停止的對話框,然後我的進程怎麼就退出了呢,老哥,你的結論不對吧。

回答:哈哈,這個問題是一個好問題,想要回答這個問題,就需要了解JVM如何處理未捕獲異常的。這也是我們之前文章JVM 如何處理未捕獲異常介紹的。

這裡簡單概括一下就是,當JVM發現異常後

  • 首先嘗試檢測當前的Thread是否有UncaughtExeptionHandler,並嘗試分發出問題的Throwable實例
  • 如果上一步找不到對應的UncaughtExceptionHandler,則分發問題的Throwable實例到其所在的ThreadGroup
  • ThreadGroup優先會將Throwable實例分發給其父ThreadGroup
  • 如果ThreadGroup沒有父ThreadGroup,則嘗試分發給所有執行緒默認使用的UncaughtExceptionHandler

所以,我們按照這個流程扒了一下RuntimeInit.java 發現了這樣的程式碼。

/**     * Use this to log a message when a thread exits due to an uncaught     * exception.  The framework catches these for the main threads, so     * this should only matter for threads created by applications.     */    private static class UncaughtHandler implements Thread.UncaughtExceptionHandler {        public void uncaughtException(Thread t, Throwable e) {            try {                // Don't re-enter -- avoid infinite loops if crash-reporting crashes.                if (mCrashing) return;                mCrashing = true;                if (mApplicationObject == null) {                    Slog.e(TAG, "*** FATAL EXCEPTION IN SYSTEM PROCESS: " + t.getName(), e);                } else {                    Slog.e(TAG, "FATAL EXCEPTION: " + t.getName(), e);                }                // 展示 應用已停止的 對話框                // Bring up crash dialog, wait for it to be dismissed                ActivityManagerNative.getDefault().handleApplicationCrash(                        mApplicationObject, new ApplicationErrorReport.CrashInfo(e));            } catch (Throwable t2) {                try {                    Slog.e(TAG, "Error reporting crash", t2);                } catch (Throwable t3) {                    // Even Slog.e() fails!  Oh well.                }            } finally {              //殺掉進程                // Try everything to make sure this process goes away.                Process.killProcess(Process.myPid());                System.exit(10);            }        }    }  

上述程式碼會執行兩個主要的操作

  • 展示一個崩潰的對話框
  • 在finally 部分,殺掉當前的進程

Android系統會在進程啟動後,通過下面的程式碼為所有的執行緒設置默認的UncaughtExceptionHandler

/* set default handler; this applies to all threads in the VM */    Thread.setDefaultUncaughtExceptionHandler(new UncaughtHandler());  

同時由於如下原因

  • 出問題的執行緒沒有通過Thread.setUncaughtExceptionHandler顯式設置對應的處理者
  • 執行緒所在的ThreadGroup實例屬於原生的ThreadGroup,而不是用戶自定義並重寫uncaughtException的ThreadGroup子類。

所以出現未捕獲的異常,默認就會走到了Android系統默認設置的所有執行緒共用的處理者。

如果發生在主執行緒中呢

前面說的都是子執行緒,那麼如果主執行緒出現未捕獲異常,進程應該會退出吧。

private static void uncaughtExceptionInMainThread() {        Utils.causeNPE();    }  

執行上面的程式碼,得到進程退出的日誌

Exception in thread "main" java.lang.NullPointerException      at Utils.causeNPE(Utils.java:35)      at Main.uncaughtExceptionInMainThread(Main.java:28)      at Main.main(Main.java:14)        Process finished with exit code 1  

可是當我們執行下面的這份程式碼(啟動另一個執行緒並休眠20秒),結果卻是不一樣的

private static void uncaughtExceptionInMainThreadNotLastUserThread() {        new Thread(new Runnable() {            @Override            public void run() {                Utils.makeThreadSleep(20 * 1000);                System.out.println("uncaughtExceptionInMainThreadNotLastUserThread time=" + Utils.getTimeForDebug()                    + ";thread=" + Thread.currentThread().getName()                );                Utils.dumpAllThreadsInfo();            }        }).start();        Utils.makeThreadSleep(5 * 1000);        System.out.println("uncaughtExceptionInMainThreadNotLastUserThread mainThread time=" + Utils.getTimeForDebug());        Utils.causeNPE();    }  

得到的日誌輸出是

uncaughtExceptionInMainThreadNotLastUserThread mainThread time=20:48:09    // 異常發生    Exception in thread "main" java.lang.NullPointerException      at Utils.causeNPE(Utils.java:35)      at Main.uncaughtExceptionInMainThreadNotLastUserThread(Main.java:44)      at Main.main(Main.java:15)    //Thread-0  執行緒休眠結束    uncaughtExceptionInMainThreadNotLastUserThread time=20:48:24;thread=Thread-0    // 列印此時的全部執行緒資訊    dumpAllThreadsInfo thread.name=Signal Dispatcher;thread.state=RUNNABLE;thread.isAlive=true;group=java.lang.ThreadGroup[name=system,maxpri=10]    dumpAllThreadsInfo thread.name=DestroyJavaVM;thread.state=RUNNABLE;thread.isAlive=true;group=java.lang.ThreadGroup[name=main,maxpri=10]    dumpAllThreadsInfo thread.name=Thread-0;thread.state=RUNNABLE;thread.isAlive=true;group=java.lang.ThreadGroup[name=main,maxpri=10]    dumpAllThreadsInfo thread.name=Monitor Ctrl-Break;thread.state=RUNNABLE;thread.isAlive=true;group=java.lang.ThreadGroup[name=main,maxpri=10]    dumpAllThreadsInfo thread.name=Reference Handler;thread.state=WAITING;thread.isAlive=true;group=java.lang.ThreadGroup[name=system,maxpri=10]    dumpAllThreadsInfo thread.name=Attach Listener;thread.state=RUNNABLE;thread.isAlive=true;group=java.lang.ThreadGroup[name=system,maxpri=10]    dumpAllThreadsInfo thread.name=Finalizer;thread.state=WAITING;thread.isAlive=true;group=java.lang.ThreadGroup[name=system,maxpri=10]    //進程退出    Process finished with exit code 1  

進程並沒有隨著主執行緒中出現未捕獲異常而理解退出,而是等到我們啟動的Thread-0結束之後才退出的。

那麼這是為什麼呢,看過我之前文章JVM 中的守護執行緒的朋友應該了解

JVM退出通常有兩種情況

  • 有效的調用System.exit()
  • 所有的非守護執行緒退出後,JVM就會自動退出

因此不難得出結論

  • 第一段程式碼中,只有主執行緒一個非守護執行緒,主執行緒銷毀,所以進程會結束
  • 第二段程式碼中,主執行緒銷毀後,還有一個Thread-0(由主執行緒啟動,所以也是一個非守護執行緒),JVM會等待其結束後而退出。

結論

所以未捕獲異常只會導致所屬執行緒銷毀,並不會導致JVM退出。這裡我還找到一份官方API文檔作為佐證。

Uncaught exceptions are handled in shutdown hooks just as in any other thread, by invoking the uncaughtException method of the thread』s ThreadGroup object. The default implementation of this method prints the exception』s stack trace to System.err and terminates the thread; it does not cause the virtual machine to exit or halt.

上面的內容來自Runtime.addShutdownHook

參考聲明