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.RuntimeException
at 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 {
@Override
public 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');
}
@Override
public synchronized Throwable fillInStackTrace() {
return this;
}
}
抛出上述异常将导致JVM打印以下错误(认真尝试!)
Exception in thread 'main' SponsoredException
at 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%的情况下,该位置相同。
- 您可以完全控制由异常返回的堆栈跟踪
- 生成堆栈跟踪会带来一些成本,但是如果它成为应用程序的瓶颈,则可能是您做错了什么。
翻译自: https://www.javacodegeeks.com/2012/10/where-do-stack-traces-come-from.html