问:Java中的异常处理真的很慢吗?
传统观点以及大量谷歌结果表明,不应该将异常逻辑用于Java中的正常程序流。通常会给出两个原因,
它真的很慢——甚至比普通代码慢一个数量级(给出的原因各不相同),
and
它很混乱,因为人们只希望在异常代码中处理错误。
这个问题是关于第一条的。
As an example, this page describes Java exception handling as "very slow" and relates the slowness to the creation of the exception message string - "this string is then used in creating the exception object that is thrown. This is not fast." The article Effective Exception Handling in Java says that "the reason for this is due to the object creation aspect of exception handling, which thereby makes throwing exceptions inherently slow". Another reason out there is that the stack trace generation is what slows it down.
My testing (using Java 1.6.0_07, Java HotSpot 10.0, on 32 bit Linux), indicates that exception handling is no slower than regular code. I tried running a method in a loop that executes some code. At the end of the method, I use a boolean to indicate whether to return or throw. This way the actual processing is the same. I tried running the methods in different orders and averaging my test times, thinking it may have been the JVM warming up. In all my tests, the throw was at least as fast as the return, if not faster (up to 3.1% faster). I am completely open to the possibility that my tests were wrong, but I haven't seen anything out there in the way of the code sample, test comparisons, or results in the last year or two that show exception handling in Java to actually be slow.
引导我走上这条路的是我需要使用的一个API,它将抛出异常作为正常控制逻辑的一部分。我想纠正它们的用法,但现在我可能做不到。我是否应该赞美他们的前瞻思维?
在论文《即时编译中的高效Java异常处理》中,作者建议,即使没有抛出异常,仅异常处理程序的存在就足以阻止JIT编译器正确优化代码,从而降低代码的速度。我还没有测试过这个理论。
我对异常速度和以编程方式检查数据的看法。
许多类都有字符串到值的转换器(扫描器/解析器),也有受人尊敬和知名的库;)
通常有形式
class Example {
public static Example Parse(String input) throws AnyRuntimeParsigException
...
}
异常名称只是例子,通常是未选中的(运行时),所以抛出声明只是我的图片
有时存在第二种形式:
public static Example Parse(String input, Example defaultValue)
不扔
当第二个文件不可用时(或者程序员读的文档太少,只使用第一个文件),用正则表达式编写这样的代码。正则表达式很酷,政治正确等:
Xxxxx.regex(".....pattern", src);
if(ImTotallySure)
{
Example v = Example.Parse(src);
}
使用这段代码,程序员没有异常成本。BUT具有相当高的代价的正则表达式ALWAYS与小的代价异常有时。
我几乎总是在这种情况下使用
try { parse } catch(ParsingException ) // concrete exception from javadoc
{
}
没有分析堆栈跟踪等,我相信在你的讲座后相当快。
不要害怕例外情况
供你参考,我扩展了Mecki做的实验:
method1 took 1733 ms, result was 2
method2 took 1248 ms, result was 2
method3 took 83997 ms, result was 2
method4 took 1692 ms, result was 2
method5 took 60946 ms, result was 2
method6 took 25746 ms, result was 2
前3个和Mecki的一样(我的笔记本电脑明显慢一些)。
method4和method3是一样的,除了它创建了一个新的Integer(1)而不是抛出一个新的Exception()。
method5类似于method3,除了它创建了新的Exception()而不抛出它。
Method6和method3很像,只是它会抛出一个预先创建的异常(一个实例变量),而不是创建一个新异常。
在Java中,抛出异常的大部分开销是收集堆栈跟踪所花费的时间,这发生在创建异常对象时。抛出异常的实际成本虽然很大,但比创建异常的成本要小得多。
It depends how exceptions are implemented. The simplest way is using setjmp and longjmp. That means all registers of the CPU are written to the stack (which already takes some time) and possibly some other data needs to be created... all this already happens in the try statement. The throw statement needs to unwind the stack and restore the values of all registers (and possible other values in the VM). So try and throw are equally slow, and that is pretty slow, however if no exception is thrown, exiting the try block takes no time whatsoever in most cases (as everything is put on the stack which cleans up automatically if the method exists).
Sun和其他人认识到,这可能是次优的,当然随着时间的推移,虚拟机会变得越来越快。还有另一种实现异常的方法,它使try本身闪电般快(实际上try本身根本不会发生任何事情——当类被VM加载时,需要发生的一切都已经完成了),并且它使throw不那么慢。我不知道哪个JVM使用了这种新的、更好的技术……
...但你是在用Java写代码,所以你的代码以后只能在一个特定系统的一个JVM上运行吗?因为如果它可以在任何其他平台或任何其他JVM版本(可能是任何其他供应商的)上运行,谁说他们也使用快速实现呢?速度快的要比速度慢的复杂得多,而且不容易在所有系统上实现。你想要便携吗?那就不要指望异常会很快。
It also makes a big difference what you do within a try block. If you open a try block and never call any method from within this try block, the try block will be ultra fast, as the JIT can then actually treat a throw like a simple goto. It neither needs to save stack-state nor does it need to unwind the stack if an exception is thrown (it only needs to jump to the catch handlers). However, this is not what you usually do. Usually you open a try block and then call a method that might throw an exception, right? And even if you just use the try block within your method, what kind of method will this be, that does not call any other method? Will it just calculate a number? Then what for do you need exceptions? There are much more elegant ways to regulate program flow. For pretty much anything else but simple math, you will have to call an external method and this already destroys the advantage of a local try block.
请看下面的测试代码:
public class Test {
int value;
public int getValue() {
return value;
}
public void reset() {
value = 0;
}
// Calculates without exception
public void method1(int i) {
value = ((value + i) / i) << 1;
// Will never be true
if ((i & 0xFFFFFFF) == 1000000000) {
System.out.println("You'll never see this!");
}
}
// Could in theory throw one, but never will
public void method2(int i) throws Exception {
value = ((value + i) / i) << 1;
// Will never be true
if ((i & 0xFFFFFFF) == 1000000000) {
throw new Exception();
}
}
// This one will regularly throw one
public void method3(int i) throws Exception {
value = ((value + i) / i) << 1;
// i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
// an AND operation between two integers. The size of the number plays
// no role. AND on 32 BIT always ANDs all 32 bits
if ((i & 0x1) == 1) {
throw new Exception();
}
}
public static void main(String[] args) {
int i;
long l;
Test t = new Test();
l = System.currentTimeMillis();
t.reset();
for (i = 1; i < 100000000; i++) {
t.method1(i);
}
l = System.currentTimeMillis() - l;
System.out.println(
"method1 took " + l + " ms, result was " + t.getValue()
);
l = System.currentTimeMillis();
t.reset();
for (i = 1; i < 100000000; i++) {
try {
t.method2(i);
} catch (Exception e) {
System.out.println("You'll never see this!");
}
}
l = System.currentTimeMillis() - l;
System.out.println(
"method2 took " + l + " ms, result was " + t.getValue()
);
l = System.currentTimeMillis();
t.reset();
for (i = 1; i < 100000000; i++) {
try {
t.method3(i);
} catch (Exception e) {
// Do nothing here, as we will get here
}
}
l = System.currentTimeMillis() - l;
System.out.println(
"method3 took " + l + " ms, result was " + t.getValue()
);
}
}
结果:
method1 took 972 ms, result was 2
method2 took 1003 ms, result was 2
method3 took 66716 ms, result was 2
try块的减速太小,无法排除后台进程等混杂因素。但是catch block杀死了一切,让它慢了66倍!
正如我所说,如果将try/catch和throw都放在同一个方法(method3)中,结果不会那么糟糕,但这是我不依赖的特殊JIT优化。即使使用这种优化,抛出仍然非常慢。我不知道你们想做什么,但肯定有比try/catch/throw更好的方法。
使用附带的代码,在JDK 15上,@Mecki测试用例得到了完全不同的结果。这基本上是在5个循环中运行代码,第一个循环稍微短一些,给VM一些时间来热身。
结果:
Loop 1 10000 cycles
method1 took 1 ms, result was 2
method2 took 0 ms, result was 2
method3 took 22 ms, result was 2
method4 took 22 ms, result was 2
method5 took 24 ms, result was 2
Loop 2 10000000 cycles
method1 took 39 ms, result was 2
method2 took 39 ms, result was 2
method3 took 1558 ms, result was 2
method4 took 1640 ms, result was 2
method5 took 1717 ms, result was 2
Loop 3 10000000 cycles
method1 took 49 ms, result was 2
method2 took 48 ms, result was 2
method3 took 126 ms, result was 2
method4 took 88 ms, result was 2
method5 took 87 ms, result was 2
Loop 4 10000000 cycles
method1 took 34 ms, result was 2
method2 took 34 ms, result was 2
method3 took 33 ms, result was 2
method4 took 98 ms, result was 2
method5 took 58 ms, result was 2
Loop 5 10000000 cycles
method1 took 34 ms, result was 2
method2 took 33 ms, result was 2
method3 took 33 ms, result was 2
method4 took 48 ms, result was 2
method5 took 49 ms, result was 2
package hs.jfx.eventstream.api;
public class Snippet {
int value;
public int getValue() {
return value;
}
public void reset() {
value = 0;
}
// Calculates without exception
public void method1(int i) {
value = ((value + i) / i) << 1;
// Will never be true
if ((i & 0xFFFFFFF) == 1000000000) {
System.out.println("You'll never see this!");
}
}
// Could in theory throw one, but never will
public void method2(int i) throws Exception {
value = ((value + i) / i) << 1;
// Will never be true
if ((i & 0xFFFFFFF) == 1000000000) {
throw new Exception();
}
}
private static final NoStackTraceRuntimeException E = new NoStackTraceRuntimeException();
// This one will regularly throw one
public void method3(int i) throws NoStackTraceRuntimeException {
value = ((value + i) / i) << 1;
// i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
// an AND operation between two integers. The size of the number plays
// no role. AND on 32 BIT always ANDs all 32 bits
if ((i & 0x1) == 1) {
throw E;
}
}
// This one will regularly throw one
public void method4(int i) throws NoStackTraceThrowable {
value = ((value + i) / i) << 1;
// i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
// an AND operation between two integers. The size of the number plays
// no role. AND on 32 BIT always ANDs all 32 bits
if ((i & 0x1) == 1) {
throw new NoStackTraceThrowable();
}
}
// This one will regularly throw one
public void method5(int i) throws NoStackTraceRuntimeException {
value = ((value + i) / i) << 1;
// i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
// an AND operation between two integers. The size of the number plays
// no role. AND on 32 BIT always ANDs all 32 bits
if ((i & 0x1) == 1) {
throw new NoStackTraceRuntimeException();
}
}
public static void main(String[] args) {
for(int k = 0; k < 5; k++) {
int cycles = 10000000;
if(k == 0) {
cycles = 10000;
try {
Thread.sleep(500);
}
catch(InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("Loop " + (k + 1) + " " + cycles + " cycles");
int i;
long l;
Snippet t = new Snippet();
l = System.currentTimeMillis();
t.reset();
for (i = 1; i < cycles; i++) {
t.method1(i);
}
l = System.currentTimeMillis() - l;
System.out.println(
"method1 took " + l + " ms, result was " + t.getValue()
);
l = System.currentTimeMillis();
t.reset();
for (i = 1; i < cycles; i++) {
try {
t.method2(i);
} catch (Exception e) {
System.out.println("You'll never see this!");
}
}
l = System.currentTimeMillis() - l;
System.out.println(
"method2 took " + l + " ms, result was " + t.getValue()
);
l = System.currentTimeMillis();
t.reset();
for (i = 1; i < cycles; i++) {
try {
t.method3(i);
} catch (NoStackTraceRuntimeException e) {
// always comes here
}
}
l = System.currentTimeMillis() - l;
System.out.println(
"method3 took " + l + " ms, result was " + t.getValue()
);
l = System.currentTimeMillis();
t.reset();
for (i = 1; i < cycles; i++) {
try {
t.method4(i);
} catch (NoStackTraceThrowable e) {
// always comes here
}
}
l = System.currentTimeMillis() - l;
System.out.println( "method4 took " + l + " ms, result was " + t.getValue() );
l = System.currentTimeMillis();
t.reset();
for (i = 1; i < cycles; i++) {
try {
t.method5(i);
} catch (RuntimeException e) {
// always comes here
}
}
l = System.currentTimeMillis() - l;
System.out.println( "method5 took " + l + " ms, result was " + t.getValue() );
}
}
public static class NoStackTraceRuntimeException extends RuntimeException {
public NoStackTraceRuntimeException() {
super("my special throwable", null, false, false);
}
}
public static class NoStackTraceThrowable extends Throwable {
public NoStackTraceThrowable() {
super("my special throwable", null, false, false);
}
}
}
关于异常性能的好文章是:
https://shipilev.net/blog/2014/exceptional-performance/
实例化vs重用现有的,有堆栈跟踪和没有,等等:
Benchmark Mode Samples Mean Mean error Units
dynamicException avgt 25 1901.196 14.572 ns/op
dynamicException_NoStack avgt 25 67.029 0.212 ns/op
dynamicException_NoStack_UsedData avgt 25 68.952 0.441 ns/op
dynamicException_NoStack_UsedStack avgt 25 137.329 1.039 ns/op
dynamicException_UsedData avgt 25 1900.770 9.359 ns/op
dynamicException_UsedStack avgt 25 20033.658 118.600 ns/op
plain avgt 25 1.259 0.002 ns/op
staticException avgt 25 1.510 0.001 ns/op
staticException_NoStack avgt 25 1.514 0.003 ns/op
staticException_NoStack_UsedData avgt 25 4.185 0.015 ns/op
staticException_NoStack_UsedStack avgt 25 19.110 0.051 ns/op
staticException_UsedData avgt 25 4.159 0.007 ns/op
staticException_UsedStack avgt 25 25.144 0.186 ns/op
根据堆栈跟踪的深度:
Benchmark Mode Samples Mean Mean error Units
exception_0000 avgt 25 1959.068 30.783 ns/op
exception_0001 avgt 25 1945.958 12.104 ns/op
exception_0002 avgt 25 2063.575 47.708 ns/op
exception_0004 avgt 25 2211.882 29.417 ns/op
exception_0008 avgt 25 2472.729 57.336 ns/op
exception_0016 avgt 25 2950.847 29.863 ns/op
exception_0032 avgt 25 4416.548 50.340 ns/op
exception_0064 avgt 25 6845.140 40.114 ns/op
exception_0128 avgt 25 11774.758 54.299 ns/op
exception_0256 avgt 25 21617.526 101.379 ns/op
exception_0512 avgt 25 42780.434 144.594 ns/op
exception_1024 avgt 25 82839.358 291.434 ns/op
有关其他详细信息(包括来自JIT的x64汇编程序),请阅读原始博客文章。
这意味着Hibernate/Spring/etc-EE-shit因为异常(xD)而变慢。
通过重写应用程序控制流,避免异常(返回错误作为返回),提高应用程序的性能10 -100倍,这取决于你抛出它们的频率))