见到有人讨论java里的异常性能是好是坏,在业务代码里要不要用异常处理.
例如一些请求参数,到底是应该人工用if/else判断,还是通过异常统一处理,还是通过注解校验,抑或是其他方式呢?
这些方式对系统的性能会有什么实际影响呢?
一般认为,java异常之所以慢,是因为需要获取当前的运行栈信息,而异常机制本身是常规性能消耗.
进而想到,一些通用的日志框架,比如log4j,logback,都是通过运行栈获取抛出异常的代码方法和运行行数的,官方文档也标注此类信息的打印会有较大的性能消耗.
而且我们也是通过类似的方式对jws的日志进行了增强.
那么性能到底会有有多差?
运行环境
jdk8 + idea 2016
mac mini 2014 中配(硬盘非常慢,如有其它运行结果请联系)
非空机运行,所以结果不是非常准确
jvm配置<code>-client -xcomp -xmx1024m -xms1024m</code>,每次调用完后主动通知gc一次
编译模式跟解析模式的结果相差非常大
一般都说,使用异常之所以慢,是因为获取运行栈慢,那到底有多慢,慢在哪里,如果不获取异常栈的话速度又如何?
通过<code>throwable</code>的源码可以得知,大部分的够着函数里都会调用<code>java.lang.throwable#fillinstacktrace()</code>方法设置当前运行的异常栈,而这个方法慢的原因有:
方法本身是<code>synchronized</code>,也就是对象级的同步
这个方法需要有一个native调用
需要获取当前运行栈的所有信息
另外,throwable的很多方法也是<code>synchronized</code>的.
(不过线程栈信息本身有缓存,理论上第二次调用会快一些,而且异常一般不会在多个线程中处理)
所以理论上来说,只要把异常栈填充这一步去掉,异常对象应该是跟普通对象是差不多的.
刚好throwalbe及各种exception都有一个特殊的构造函数,可以跳过异常栈的获取.
普通对象
hashmap
省略异常栈
普通异常
1000000个对象
35
38
48
1444
10000000个对象
30
62
99
10525
100000000个对象
269
593
936
112262
可以看到,关闭异常栈获取后,异常对象的创建跟普通对象基本一致,而获取异常栈则多了不止一个数量级的耗时.
10个线程,每个线程10000000个对象
223
728
534
73946
100个线程,每个线程1000000个对象
251
605
533
85106
500个线程,每个线程200000个对象
264
780
793
68589
在并行模式下的结果跟串行模式几乎一致.
如果异常对象的创建的性能没有问题,那么try/catch块呢?
没有try/catch
循环外try/catch
循环内try/catch
循环内3次try/catch
运行10000000次
10
12
9
13
运行100000000次
43
49
53
45
运行500000000次
221
222
229
可以看到,当没有发生异常时,try/catch并不消耗性能.
那么,对异常的throw/catch需要消耗多少性能呢?
没有throw/catch
throw/catch
11
36
51
113
252
519
而当抛出异常时,try/catch本身的性能消耗也只是普通的代码性能消耗.
通用的日志框架里都是通过运行栈去获取日志调用代码的方法名/行数等信息的,而且通过上述测试可以知道,这种方式会对性能代码比较大的影响.
通过往某个文件打印日志对运行信息获取的性能做测试.(使用非缓存式打印,一般日志框架默认都没有使用缓存打印)
普通日志打印
运行信息日志打印
打印10000行
297
打印100000行
535
2132
打印500000行
2576
8598
在串行模式下,通过运行栈获取调用信息确实会比普通的日志打印耗时多,可是可能因为磁盘io关系,性能差异没有异常对象与普通对象的差异大.
10个线程,每个线程打印100000行
7199
19511
20个线程,每个线程打印50000行
7600
20053
50个线程,每个线程打印20000行
7832
19031
在并发环境下,由于性能进一步被文件io限制,性能差异进一步缩小.
以上所有的测试都只是验证在一个非常纯粹的环境下的性能表现,其中甚至会有jvm的一些优化措施.
而实际业务处理中,会有框架,网络,业务处理等多种因素会影响系统的性能.
所以通过在本机搭建的一个基于jws的web工程来模拟实际的业务服务,通过ab测试相关的场景性能.
建立一个有3个参数的请求,通过ab工具测试if/else和try/catch两种参数校验方式对性能的影响.
(数据单位:qps)
if/else参数校验
try/catch优化异常参数校验
try/catch普通异常参数校验
串行,5000请求
166.140
163.773
164.636
10并发,5000请求
206.59
206.22
203.06
50并发,5000请求
205.74
205.64
202.80
根据统计平台,生产环境80%请求的耗时基本都在10ms附近,测试用例通过<code>sleep</code>的方式模拟每次请求有10ms的业务处理.
(考虑到机器问题,在本机器上10ms的业务处理应该算是一个很小的值了)
每次请求打印10行日志
每次请求打印20行日志
每次请求打印50行日志
普通日志
56.67
54.85
49.76
运行上线文日志
51.39
47.44
37.10
59.54
57.27
52.78
55.81
48.96
38.57
虽然即使经过优化后的异常性能也有一定的消耗,可是在异常校验这种场景下,一来因为发生频率小,二来因为执行次数少(一个请求只会出现一次try/catch),对业务请求的性能几乎没有影响.
而通过运行栈实现的日志增强,因为日志打印本身的高频率,所以对业务系统性能有一定的影响.
(此用例基于jws,非jws环境无法运行)