堆栈跟踪从何而来?

我认为,阅读和理解堆栈跟踪是每个程序员都必须具备的一项基本技能,以便有效地解决每种JVM语言的问题(另请参阅: 过滤日志中无关的堆栈跟踪行和首先记录引起异常的根本原因 )。 那么我们可以从一个小测验开始吗? 给定以下代码,堆栈跟踪中将出现哪些方法? 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%的情况下,该位置相同。
  • 您可以完全控制由异常返回的堆栈跟踪
  • 生成堆栈跟踪会带来一些成本,但是如果它成为应用程序的瓶颈,则可能是您做错了什么。

参考: 堆栈跟踪来自何处? 来自我们的JCG合作伙伴 Tomasz Nurkiewicz,来自Java和邻里博客。


翻译自: https://www.javacodegeeks.com/2012/10/where-do-stack-traces-come-from.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值