在Java中,当实际上没有错误时使用throw / catch作为逻辑的一部分通常是一个坏主意(部分)因为抛出和捕获异常是昂贵的,并且在循环中多次执行它通常比其他更慢控制结构,不涉及抛出异常 .
我的问题是,抛出/捕获本身或创建Exception对象时产生的成本(因为它获得了很多运行时信息,包括执行堆栈)?
换句话说,如果我这样做
Exception e = new Exception();
但是不要扔它,是投掷的大部分成本,还是投掷捕获处理成本高昂?
我不是在将代码放入try / catch块中是否会增加执行该代码的成本,我问的是,捕获Exception是否是昂贵的部分,或者创建(调用构造函数)Exception是否是昂贵的部分 .
问这个问题的另一种方法是,如果我创建了一个Exception实例并且一遍又一遍地抛出它,那么这会比每次抛出时创建一个新的Exception快得多吗?
6 回答
Creating 异常对象并不比创建其他常规对象更昂贵 . 主要成本隐藏在本机fillInStackTrace方法中,该方法遍历调用堆栈并收集构建堆栈跟踪所需的所有信息:类,方法名称,行号等 .
关于高异常成本的神话来自于大多数
Throwable
构造函数隐含地调用fillInStackTrace
. 但是,有一个constructor来创建没有堆栈跟踪的Throwable
. 它允许您制作非常快速实例化的throwable . 创建轻量级异常的另一种方法是覆盖fillInStackTrace
.那么 throwing 怎么样?
实际上,它取决于抛出异常的位置 caught .
如果它被捕获在相同的方法中(或者更确切地说,在相同的上下文中,因为上下文可以包括由于内联的几个方法),那么
throw
与goto
一样快(和当然,在JIT编译之后) .但是,如果
catch
块位于堆栈的更深处,那么JVM需要展开堆栈帧,这可能需要更长的时间 . 如果涉及synchronized
块或方法,则需要更长的时间,因为展开意味着释放由移除的堆栈帧拥有的监视器 .我可以通过适当的基准来确认上述陈述,但幸运的是我不需要这样做,因为HotSpot性能工程师AlexeyShipilëv的帖子已经完全涵盖了所有方面:The Exceptional Performance of Lil' Exception .
大多数
Throwable
构造函数中的第一个操作是fill in the stack trace,,这是大部分费用的地方 .但是,有一个受保护的构造函数,其中包含一个禁用堆栈跟踪的标志 . 扩展
Exception
时也可以访问This constructor . 如果创建自定义异常类型,则可以避免创建堆栈跟踪并以更少的信息为代价获得更好的性能 .如果通过常规方法创建任何类型的单个异常,则可以多次重新抛出它,而无需填充堆栈跟踪 . 但是,它的堆栈跟踪将反映它的构造位置,而不是它在特定实例中抛出的位置 .
当前版本的Java尝试优化堆栈跟踪创建 . 调用本机代码以填充堆栈跟踪,该跟踪以较轻的本机结构记录跟踪 . 仅当
getStackTrace()
,printStackTrace()
或其他需要跟踪的方法被调用时,才会从此记录延迟创建相应的Java StackTraceElement对象 .如果消除堆栈跟踪生成,另一个主要成本是在throw和catch之间展开堆栈 . 在捕获异常之前遇到的中间帧越少,这将越快 .
设计您的程序,以便仅在真正特殊情况下抛出异常,并且很难证明这些优化是合理的 .
这里有关于例外的好文章 .
http://shipilev.net/blog/2014/exceptional-performance/
结论是堆栈跟踪结构和堆栈展开是昂贵的部分 . 下面的代码利用了
1.7
中的一个功能,我们可以打开和关闭堆栈跟踪 . 然后我们可以使用它来查看不同场景的成本以下是单独创建对象的时间 . 我在这里添加了
String
所以你可以看到,如果没有写入堆栈,创建JavaException
对象和String
几乎没有区别 . 随着堆栈写入开启,差异是显着的,即至少慢一个数量级 .以下显示了从特定深度的投掷返回一百万次所需的时间 .
以下几乎可以肯定是过度简化......
如果我们在堆栈写入时深度为16,那么对象创建大约需要大约40%的时间,实际的堆栈跟踪占绝大多数这个 . ~93%实例化JavaException对象是由于采用了堆栈跟踪 . 这意味着在这种情况下展开堆栈占用了其他50%的时间 .
当我们关闭堆栈跟踪对象创建帐户的小得多,即20%,堆栈展开现在占80%的时间 .
在这两种情况下,堆栈展开占用了总时间的很大一部分 .
与您通常找到的相比,此示例中的堆栈帧很小 .
您可以使用javap查看字节码
即这是方法4 ...
使用
null
堆栈跟踪创建Exception
所需的时间与throw
和try-catch
块一样多 . 但是, filling the stack trace takes on average 5x longer .我创建了以下基准来演示对性能的影响 . 我将
-Djava.compiler=NONE
添加到运行配置以禁用编译器优化 . 为了衡量构建堆栈跟踪的影响,我扩展了Exception
类以利用无堆栈构造函数:基准代码如下:
Output:
这意味着创建
NoStackException
与重复抛出相同的Exception
一样昂贵 . 它还显示创建Exception
并填充其堆栈跟踪大约需要 4x .这部分问题......
似乎在询问是否创建异常并在某处缓存它可以提高性能 . 是的,它确实 . 这与关闭正在创建对象的堆栈是一样的,因为它已经完成了 .
这些是我得到的时间,请在此之后阅读警告......
当然,问题是你的堆栈跟踪现在指向你实例化对象的位置,而不是它被抛出的位置 .
以@ AustinD的答案为出发点,我做了一些调整 . 代码在底部 .
除了添加重复抛出一个Exception实例的情况之外,我还关闭了编译器优化,以便我们可以获得准确的性能结果 . 我按照this answer将
-Djava.compiler=NONE
添加到VM参数中 . (在eclipse中,编辑Run Configuration→Arguments以设置此VM参数)结果:
所以创建异常的成本大约是抛出它的5倍 . 假设编译器没有优化大部分成本 .
为了比较,这里是相同的测试运行而没有禁用优化:
码: