聊一聊未捕獲異常與進程退出的關聯
- 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