引发异常的哪一部分成本高昂?

2022-08-31 05:25:46

在Java中,当实际上没有错误时,使用 throw/catch 作为逻辑的一部分通常是一个坏主意(部分原因是因为抛出和捕获异常是昂贵的,并且在循环中多次这样做通常比其他不涉及抛出异常的控制结构慢得多。

我的问题是,是在 throw/catch 本身中产生的成本,还是在创建 Exception 对象时产生的成本(因为它获取了大量运行时信息,包括执行堆栈)?

换句话说,如果我这样做

Exception e = new Exception();

但不要扔它,是投掷的大部分成本,还是投掷+接球处理的成本是多少?

我不是在问将代码放在 try/catch 块中是否会增加执行该代码的成本,而是在问捕获异常是否是昂贵的部分,或者创建(调用构造函数)异常是昂贵的部分。

另一种提问方式是,如果我制作了一个 Exception 实例,并一遍又一遍地抛出并捕获它,这会比每次抛出一个新的 Exception 快得多吗?


答案 1

创建异常对象不一定比创建其他常规对象更昂贵。主要成本隐藏在本机 fillInStackTrace 方法中,该方法遍历调用堆栈并收集构建堆栈跟踪所需的所有信息:类、方法名称、行号等。

大多数构造函数隐式调用 。这就是创建异常很慢的想法的来源。但是,有一个构造函数可以创建不带堆栈跟踪的构造函数。它允许您制作非常快速的可抛出物实例化。创建轻量级异常的另一种方法是覆盖 。ThrowablefillInStackTraceThrowablefillInStackTrace


现在抛出一个异常怎么样?
实际上,这取决于捕获引发异常的位置。

如果它被捕获在相同的方法中(或者更准确地说,在同一上下文中,因为上下文由于内联可以包含多个方法),那么它就像(当然,在JIT编译之后)一样快速和简单。throwgoto

但是,如果一个块位于堆栈的更深处,那么JVM需要展开堆栈帧,这可能需要更长的时间。如果涉及块或方法,则需要更长的时间,因为展开意味着释放由已删除的堆栈帧拥有的监视器。catchsynchronized


我可以通过适当的基准测试来确认上述陈述,但幸运的是我不需要这样做,因为在HotSpot的性能工程师Alexey Shipilev:Lil' Exception的卓越性能的帖子中已经完美地涵盖了所有方面。


答案 2

大多数构造函数中的第一个操作是填充堆栈跟踪,这是大部分费用所在。Throwable

但是,有一个受保护的构造函数,该构造函数具有禁用堆栈跟踪的标志。此构造函数在扩展时也可访问。如果您创建自定义异常类型,则可以避免创建堆栈跟踪,并以更少的信息为代价获得更好的性能。Exception

如果通过正常方式创建任何类型的单个异常,则可以多次重新引发它,而不会产生填充堆栈跟踪的开销。但是,它的堆栈跟踪将反映它的构造位置,而不是它在特定实例中引发的位置。

当前版本的 Java 尝试优化堆栈跟踪创建。调用本机代码来填充堆栈跟踪,堆栈跟踪以权重较轻的本机结构记录跟踪。相应的 Java StackTraceElement 对象仅在调用 、 或需要跟踪的其他方法时,才会从此记录中延迟创建。getStackTrace()printStackTrace()

如果消除堆栈跟踪生成,则另一个主要成本是在抛出和捕获之间展开堆栈。在捕获异常之前遇到的干预帧越少,这速度就越快。

设计你的程序,以便只在真正特殊的情况下抛出异常,并且像这样的优化很难证明是合理的。


推荐