foo()
, bar()
還是兩者皆有? public class Main {public static void main(String[] args) throws IOException {try {foo();} catch (RuntimeException e) {bar(e);}}private static void foo() {throw new RuntimeException('Foo!');}private static void bar(RuntimeException e) {throw e;}
}
在C#中,根據在bar()
重新拋出原始異常的方式,兩種答案都是可能的– throw e
用再次拋出該異常的位置 bar()
在bar()
覆蓋原始堆棧跟蹤(起源于foo()
bar()
)。 。 另一方面,裸' throw
'關鍵字會重新引發異常,從而保持原始堆棧跟蹤。 Java遵循第二種方法(使用第一種方法的語法),甚至不允許直接使用前一種方法。 但是這個經過稍微修改的版本呢:
public static void main(String[] args) throws IOException {final RuntimeException e = foo();bar(e);
}private static RuntimeException foo() {return new RuntimeException();
}private static void bar(RuntimeException e) {throw e;
}
foo()
僅創建異常,而不是引發異常,而是返回該異常對象。 然后從完全不同的方法引發此異常。 堆棧跟蹤現在將如何顯示? 令人驚訝的是,它仍然指向foo()
,就像從那里拋出異常一樣,與第一個示例完全相同:
Exception in thread 'main' java.lang.RuntimeExceptionat Main.foo(Main.java:7)at Main.main(Main.java:15)
您可能會問發生了什么事? 看起來當拋出異常時不是生成堆棧跟蹤,而是在創建異常對象時生成 。 在絕大多數情況下,這些動作都發生在同一位置,因此沒有人打擾。 許多新手Java程序員甚至都不知道可以創建一個異常對象并將其分配給變量或字段,甚至可以將其傳遞出去。
但是,異常堆棧跟蹤的真正來源是什么? 答案很簡單,來自Throwable.fillInStackTrace()
方法!
public class Throwable implements Serializable {public synchronized native Throwable fillInStackTrace();//...
}
請注意,此方法不是final
方法,這使我們可以進行一點修改。 我們不僅可以繞過堆棧跟蹤的創建并在沒有任何上下文的情況下引發異常,甚至可以完全覆蓋堆棧!
public class SponsoredException extends RuntimeException {@Overridepublic synchronized Throwable fillInStackTrace() {setStackTrace(new StackTraceElement[]{new StackTraceElement('ADVERTISEMENT', ' If you don't ', null, 0),new StackTraceElement('ADVERTISEMENT', ' want to see this ', null, 0),new StackTraceElement('ADVERTISEMENT', ' exception ', null, 0),new StackTraceElement('ADVERTISEMENT', ' please buy ', null, 0),new StackTraceElement('ADVERTISEMENT', ' full version ', null, 0),new StackTraceElement('ADVERTISEMENT', ' of the program ', null, 0)});return this;}
}public class ExceptionFromHell extends RuntimeException {public ExceptionFromHell() {super('Catch me if you can');}@Overridepublic synchronized Throwable fillInStackTrace() {return this;}
}
拋出上述異常將導致JVM打印以下錯誤(認真嘗試!)
Exception in thread 'main' SponsoredExceptionat ADVERTISEMENT. If you don't (Unknown Source)at ADVERTISEMENT. want to see this (Unknown Source)at ADVERTISEMENT. exception (Unknown Source)at ADVERTISEMENT. please buy (Unknown Source)at ADVERTISEMENT. full version (Unknown Source)at ADVERTISEMENT. of the program (Unknown Source)Exception in thread 'main' ExceptionFromHell: Catch me if you can
那就對了。 ExceptionFromHell
更加有趣。 由于它不包括堆棧跟蹤作為異常對象的一部分,因此僅類名和消息可用。 堆棧跟蹤丟失了,JVM和任何日志記錄框架都無法對此做任何事情。 你到底為什么要這么做(我不是在談論SponsoredException
)?
某些人(?)意外地認為生成堆棧跟蹤很昂貴,這是一種native
方法,它必須遍歷整個堆棧才能構建StackTraceElement
。 我一生中曾經看到過使用這種技術的庫來更快地引發異常。 因此,我編寫了一個快速的游標卡程序基準測試,以查看拋出正常的RuntimeException
和未填充堆棧跟蹤的異常與普通方法的返回值之間的性能差異。 我使用遞歸運行具有不同堆棧跟蹤深度的測試:
public class StackTraceBenchmark extends SimpleBenchmark {@Param({'1', '10', '100', '1000'})public int threadDepth;public void timeWithoutException(int reps) throws InterruptedException {while(--reps >= 0) {notThrowing(threadDepth);}}private int notThrowing(int depth) {if(depth <= 0)return depth;return notThrowing(depth - 1);}//--------------------------------------public void timeWithStackTrace(int reps) throws InterruptedException {while(--reps >= 0) {try {throwingWithStackTrace(threadDepth);} catch (RuntimeException e) {}}}private void throwingWithStackTrace(int depth) {if(depth <= 0)throw new RuntimeException();throwingWithStackTrace(depth - 1);}//--------------------------------------public void timeWithoutStackTrace(int reps) throws InterruptedException {while(--reps >= 0) {try {throwingWithoutStackTrace(threadDepth);} catch (RuntimeException e) {}}}private void throwingWithoutStackTrace(int depth) {if(depth <= 0)throw new ExceptionFromHell();throwingWithoutStackTrace(depth - 1);}//--------------------------------------public static void main(String[] args) {Runner.main(StackTraceBenchmark.class, new String[]{'--trials', '1'});}}
結果如下:

我們可以清楚地看到,堆棧跟蹤越長,拋出異常所花費的時間就越長。 我們還看到,對于合理的堆棧跟蹤長度,拋出異常的時間不應超過100?s(比讀取1 MiB主內存快)。 最終,在沒有堆棧跟蹤的情況下拋出異常的速度提高了2-5倍。 但老實說,如果這對您來說是個問題,那么問題就出在別的地方。 如果您的應用程序經常拋出異常而實際上必須對其進行優化,則您的設計可能存在問題。 然后不要修復Java,它不會損壞。
摘要:
- 堆棧跟蹤始終顯示創建異常(對象)的位置,而不是引發異常的位置-盡管在99%的情況下,該位置相同。
- 您可以完全控制由異常返回的堆棧跟蹤
- 生成堆棧跟蹤會帶來一些成本,但是如果它成為應用程序的瓶頸,則可能是您做錯了什么。
參考: 堆棧跟蹤來自何處? 來自我們的JCG合作伙伴 Tomasz Nurkiewicz,來自Java和鄰里博客。
翻譯自: https://www.javacodegeeks.com/2012/10/where-do-stack-traces-come-from.html