天天看点

Java异常及相关调用性能测试问题测试场景结论测试代码

见到有人讨论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环境无法运行)